diff --git a/.eslintrc.js b/.eslintrc.js index 2ce6d279d93a9..dc2eaa993ce8b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -109,7 +109,7 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/lens/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/lens/**/*.{js,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', 'react-hooks/rules-of-hooks': 'off', @@ -536,9 +536,15 @@ module.exports = { * ML overrides */ { - files: ['x-pack/legacy/plugins/ml/**/*.js'], + files: ['x-pack/plugins/ml/**/*.js'], rules: { 'no-shadow': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + packageDir: './x-pack', + }, + ], }, }, @@ -728,7 +734,7 @@ module.exports = { * Lens overrides */ { - files: ['x-pack/legacy/plugins/lens/**/*.{ts,tsx}', 'x-pack/plugins/lens/**/*.{ts,tsx}'], + files: ['x-pack/plugins/lens/**/*.{ts,tsx}'], rules: { '@typescript-eslint/no-explicit-any': 'error', }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 267f3dde0b66f..086d4ccf81b10 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,8 +3,8 @@ # For more info, see https://help.github.com/articles/about-codeowners/ # App -/x-pack/legacy/plugins/lens/ @elastic/kibana-app -/x-pack/legacy/plugins/graph/ @elastic/kibana-app +/x-pack/plugins/lens/ @elastic/kibana-app +/x-pack/plugins/graph/ @elastic/kibana-app /src/legacy/server/url_shortening/ @elastic/kibana-app /src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app @@ -12,13 +12,13 @@ /src/legacy/core_plugins/kibana/public/visualize/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app -/src/legacy/core_plugins/metrics/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app -/src/legacy/core_plugins/vis_type_xy/ @elastic/kibana-app +/src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app +/src/plugins/vis_type_timeseries/ @elastic/kibana-app # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon @@ -80,6 +80,8 @@ /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui +/x-pack/legacy/plugins/uptime @elastic/uptime +/x-pack/plugins/uptime @elastic/uptime # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui @@ -95,6 +97,7 @@ # Maps /x-pack/legacy/plugins/maps/ @elastic/kibana-gis +/x-pack/plugins/maps/ @elastic/kibana-gis /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis @@ -127,7 +130,6 @@ /packages/kbn-config-schema/ @elastic/kibana-platform /src/legacy/server/config/ @elastic/kibana-platform /src/legacy/server/http/ @elastic/kibana-platform -/src/legacy/server/i18n/ @elastic/kibana-platform /src/legacy/server/logging/ @elastic/kibana-platform /src/legacy/server/saved_objects/ @elastic/kibana-platform /src/legacy/server/status/ @elastic/kibana-platform @@ -146,7 +148,10 @@ /x-pack/test/api_integration/apis/security/ @elastic/kibana-security # Kibana Localization -/src/dev/i18n/ @elastic/kibana-localization +/src/dev/i18n/ @elastic/kibana-localization +/src/legacy/server/i18n/ @elastic/kibana-localization +/src/core/public/i18n/ @elastic/kibana-localization +/packages/kbn-i18n/ @elastic/kibana-localization # Pulse /packages/kbn-analytics/ @elastic/pulse diff --git a/.sass-lint.yml b/.sass-lint.yml index dd7bc0576692b..5c2c88a1dad5d 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -8,10 +8,11 @@ files: - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' + - 'x-pack/plugins/lens/**/*.s+(a|c)ss' + - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' + - 'x-pack/plugins/maps/**/*.s+(a|c)ss' ignore: - 'x-pack/legacy/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/lens/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' rules: quotes: - 2 diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index add6f601489e1..f89f994e59e57 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -1,14 +1,11 @@ +[role="xpack"] [[advanced-queries]] -=== Advanced queries +=== Query your data -When querying in the APM app, you're simply searching and selecting data from fields in Elasticsearch documents. -Queries entered into the query bar are also added as parameters to the URL, -so it's easy to share a specific query or view with others. - -You can begin to see some of the transaction fields available for filtering: - -[role="screenshot"] -image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] +Querying your APM data is a powerful tool that can make finding bottlenecks in your code even easier. +Imagine you have a user that complains about a slow response time in a specific service. +With the query bar, you can easily filter the APM app to only display trace data for that user, +or, to only show transactions that are slower than a specified time threshold. [float] ==== Example APM app queries @@ -17,15 +14,24 @@ image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app i * Filter by response status code: `context.response.status_code >= 400` * Filter by single user ID: `context.user.id : 12` +When querying in the APM app, you're merely searching and selecting data from fields in Elasticsearch documents. +Queries entered into the query bar are also added as parameters to the URL, +so it's easy to share a specific query or view with others. + +When you type, you can begin to see some of the transaction fields available for filtering: + +[role="screenshot"] +image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] + TIP: Read the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation to learn more about the capabilities of the {kib} query language. [float] [[discover-advanced-queries]] === Querying in Discover -It may also be helpful to view your APM data in {kibana-ref}/discover.html[*Discover*]. +Alternatively, you can query your APM documents in {kibana-ref}/discover.html[*Discover*]. Querying documents in *Discover* works the same way as querying in the APM app, -and all of the example APM app queries can also be used in *Discover*. +and *Discover* supports all of the example APM app queries shown on this page. [float] ==== Example Discover query @@ -33,7 +39,7 @@ and all of the example APM app queries can also be used in *Discover*. One example where you may want to make use of *Discover*, is for viewing _all_ transactions for an endpoint, instead of just a sample. -TIP: Starting in v7.6, you can view 10 samples per bucket in the APM app, instead of just one. +TIP: Starting in v7.6, you can view ten samples per bucket in the APM app, instead of just one. Use the APM app to find a transaction name and time bucket that you're interested in learning more about. Then, switch to *Discover* and make a search: diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index 0d2834c1a400e..d911c2154ea4c 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -1,12 +1,16 @@ [role="xpack"] [[agent-configuration]] -=== APM Agent configuration +=== APM Agent central configuration -APM Agent configuration allows you to fine-tune your agent configuration directly in Kibana. -Best of all, changes are automatically propagated to your APM agents so there's no need to redeploy. +++++ +Configure APM agents with central config +++++ -To get started, simply choose the services and environments you wish to configure. -The APM app will let you know when your configurations have been applied by your agents. +APM Agent configuration allows you to fine-tune your agent configuration from within the APM app. +Changes are automatically propagated to your APM agents, so there's no need to redeploy. + +To get started, choose the services and environments you wish to configure. +The APM app will let you know when your agents have applied your configurations. [role="screenshot"] image::apm/images/apm-agent-configuration.png[APM Agent configuration in Kibana] @@ -14,29 +18,28 @@ image::apm/images/apm-agent-configuration.png[APM Agent configuration in Kibana] [float] ==== Precedence -Configurations set with APM Agent configuration take precedence over configurations set locally in the Agent. +Configurations set from the APM app take precedence over configurations set locally in each Agent. However, if APM Server is slow to respond, is offline, reports an error, etc., APM agents will use local defaults until they're able to update the configuration. -For this reason, it is still important to set custom default configurations locally in each of your agents. +For this reason, it is still essential to set custom default configurations locally in each of your agents. [float] ==== APM Server setup This feature requires {apm-server-ref}/setup-kibana-endpoint.html[Kibana endpoint configuration] in APM Server. -Why is additional configuration needed in APM Server? -That's because APM Server acts as a proxy between the agents and Kibana. +APM Server acts as a proxy between the agents and Kibana. Kibana communicates any changed settings to APM Server so that your agents only need to poll APM Server to determine which settings have changed. [float] ==== Supported configurations -Each Agent has its own list of supported configurations. +Each Agent has a list of supported configurations. After selecting a Service name and environment in the APM app, -a list of all available configuration options, +a list of all supported configuration options, including descriptions and default values, will be displayed. -Supported configurations are also marked in each Agent's configuration documentation: +Supported configurations are also tagged with the image:./images/dynamic-config.svg[] badge in each Agent's configuration reference: [horizontal] Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index a8f4f4bf0baaa..93733f5990a46 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -1,6 +1,10 @@ [role="xpack"] [[apm-api]] -== API +== APM app API + +++++ +REST API +++++ Some APM app features are provided via a REST API: diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index b8552c007b13d..75ce5f56c96c6 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -1,12 +1,16 @@ [role="xpack"] [[apm-alerts]] -=== Create an alert +=== Alerts + +++++ +Create an alert +++++ beta::[] -The APM app is integrated with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. -It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use, -and allows all alerts to be centrally managed from <>. +The APM app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. +It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use +and enables central management of all alerts from <>. [role="screenshot"] image::apm/images/apm-alert.png[Create an alert in the APM app] @@ -28,9 +32,9 @@ This guide creates an alert for the `opbeans-java` service based on the followin From the APM app, navigate to the `opbeans-java` service and select **Alerts** > **Create threshold alert** > **Transaction duration**. -The name of your alert will automatically be set as `Transaction duration | opbeans-java`, -and the alert will be tagged with `apm` and `service.name:opbeans-java`. -Feel free to edit either of these defaults. +`Transaction duration | opbeans-java` is automatically set as the name of the alert, +and `apm` and `service.name:opbeans-java` are added as tags. +It's fine to change the name of the alert, but do not edit the tags. Based on the alert criteria, define the following alert details: @@ -42,7 +46,7 @@ Based on the alert criteria, define the following alert details: * **FOR THE LAST** - `5 minutes` Select an action type. -Multiple action types can be selected, but in this example we want to post to a slack channel. +Multiple action types can be selected, but in this example, we want to post to a Slack channel. Select **Slack** > **Create a connector**. Enter a name for the connector, and paste the webhook URL. @@ -63,9 +67,9 @@ This guide creates an alert for the `opbeans-python` service based on the follow From the APM app, navigate to the `opbeans-python` service and select **Alerts** > **Create threshold alert** > **Error rate**. -The name of your alert will automatically be set as `Error rate | opbeans-python`, -and the alert will be tagged with `apm` and `service.name:opbeans-python`. -Feel free to edit either of these defaults. +`Error rate | opbeans-python` is automatically set as the name of the alert, +and `apm` and `service.name:opbeans-python` are added as tags. +It's fine to change the name of the alert, but do not edit the tags. Based on the alert criteria, define the following alert details: @@ -93,5 +97,5 @@ From this page, you can create, edit, disable, mute, and delete alerts, and crea See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more information. NOTE: If you are using an **on-premise** Elastic Stack deployment with security, -TLS must be configured for communication between Elasticsearch and Kibana. +communication between Elasticsearch and Kibana must have TLS configured. More information is in the alerting {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[prerequisites]. \ No newline at end of file diff --git a/docs/apm/bottlenecks.asciidoc b/docs/apm/bottlenecks.asciidoc deleted file mode 100644 index fbde3e9ddcbd6..0000000000000 --- a/docs/apm/bottlenecks.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[role="xpack"] -[[apm-bottlenecks]] -== Visualizing Application Bottlenecks - -Elastic APM captures different types of information from within instrumented applications: - -* {apm-overview-ref-v}/transaction-spans.html[*Spans*] contain information about a specific code path that has been executed. -They measure from the start to end of an activity, -and they can have a parent/child relationship with other spans. -* {apm-overview-ref-v}/transactions.html[*Transactions*] are a special kind of span that have extra metadata associated with them. -You can think of transactions as the highest level of work you’re measuring within a service. -As an example, a transaction could be a request to your server, a batch job, or a custom transaction type. -* {apm-overview-ref-v}/errors.html[*Errors*] contain information about the original exception that occurred or about a log created when the exception occurred. - -Each of these information types have a specific page associated with them in the APM app. -These various pages display the captured data in curated charts and tables that allow you to easily compare and debug your applications. - -For example, you can see information about response times, requests per minute, and status codes per endpoint. -You can even dive into a specific request sample and get a complete waterfall view of what your application is spending its time on. -You might see that your bottlenecks are in database queries, cache calls, or external requests. -For each incoming request and each application error, -you can also see contextual information such as the request header, user information, -system values, or custom data that you manually attached to the request. - -Having access to application-level insights with just a few clicks can drastically decrease the time you spend debugging errors, slow response times, and crashes. diff --git a/docs/apm/custom-links.asciidoc b/docs/apm/custom-links.asciidoc index 75c1c9d0009a2..4fdf39b643f94 100644 --- a/docs/apm/custom-links.asciidoc +++ b/docs/apm/custom-links.asciidoc @@ -1,6 +1,11 @@ +[role="xpack"] [[custom-links]] === Custom links +++++ +Create custom links +++++ + Elastic's custom link feature allows you to easily create up to 500 dynamic links based on your specific APM data. Custom links can be filtered to only appear in the APM app for relevant services, @@ -12,7 +17,7 @@ Ready to dive in? Jump straight to the <>. [[custom-links-create]] === Create a link -Each custom link consists of a label, url, and optional filter. +Each custom link consists of a label, URL, and optional filter. The easiest way to create a custom link is from within the actions dropdown in the transaction detail page. This method will automatically apply filters, scoping the link to that specific service, environment, transaction type, and transaction name. @@ -25,8 +30,7 @@ and selecting **Create custom link**. ==== Label The name of your custom link. -This text will be shown in the actions context menu, -so keep it as short as possible. +The actions context menu displays this text, so keep it as short as possible. TIP: Custom links are displayed alphabetically in the actions menu. @@ -39,8 +43,8 @@ URLs support dynamic field name variables, encapsulated in double curly brackets These variables will be replaced with transaction metadata when the link is clicked. Because everyone's data is different, -you'll need to examine your own traces to see what metadata is available for use. -The easiest way to do this is to select a trace in the APM app, and click **Metadata** in the **Trace Sample** table. +you'll need to examine your traces to see what metadata is available for use. +To do this, select a trace in the APM app, and click **Metadata** in the **Trace Sample** table. [role="screenshot"] image::apm/images/example-metadata.png[Example metadata] @@ -49,7 +53,7 @@ image::apm/images/example-metadata.png[Example metadata] [[custom-links-filters]] ==== Filters -Filter each link to only appear so it only appears for specific services or transactions. +Filter each link to only appear for specific services or transactions. You can filter on the following fields: * `service.name` @@ -57,7 +61,7 @@ You can filter on the following fields: * `transaction.type` * `transaction.name` -Multiple values are allowed when comma separated. +Multiple values are allowed when comma-separated. [float] [[custom-links-examples]] @@ -68,7 +72,7 @@ Multiple values are allowed when comma separated. :github-query-params: https://help.github.com/en/github/managing-your-work-on-github/about-automation-for-issues-and-pull-requests-with-query-parameters Not sure where to start with custom links? -Take a look at the examples below, and customize them to your liking! +Take a look at the examples below and customize them to your liking! [float] [[custom-links-examples-email]] diff --git a/docs/apm/deployment-annotations.asciidoc b/docs/apm/deployment-annotations.asciidoc new file mode 100644 index 0000000000000..6feadf8463226 --- /dev/null +++ b/docs/apm/deployment-annotations.asciidoc @@ -0,0 +1,17 @@ +[role="xpack"] +[[transactions-annotations]] +=== Track deployments with annotations + +++++ +Track deployments +++++ + +For enhanced visibility into your deployments, we offer deployment annotations on all transaction charts. +This feature automatically tags new deployments, so you can easily see if your deploy has increased response times +for an end-user, or if the memory/CPU footprint of your application has changed. +Being able to identify bad deployments quickly enables you to rollback and fix issues without causing costly outages. + +Deployment annotations are automatically enabled, and appear when the `service.version` of your app changes. + +[role="screenshot"] +image::apm/images/apm-transaction-annotation.png[Example view of transactions annotation in the APM app in Kibana] diff --git a/docs/apm/error-reports-watcher.asciidoc b/docs/apm/error-reports-watcher.asciidoc new file mode 100644 index 0000000000000..f41597932b751 --- /dev/null +++ b/docs/apm/error-reports-watcher.asciidoc @@ -0,0 +1,18 @@ +[role="xpack"] +[[errors-alerts-with-watcher]] +=== Error reports with Watcher + +++++ +Enable error reports +++++ + +You can use the power of the alerting features with Watcher to get reports on error occurrences. +The Watcher assistant, which is available on the errors overview, can help you set up a watch per service. + +Configure the watch with an occurrences threshold, time interval, and the desired actions, such as email or Slack notifications. +With Watcher, your team can set up reports within minutes. + +Watches are managed separately in the dedicated Watcher UI available in Advanced Settings. + +[role="screenshot"] +image::apm/images/apm-errors-watcher-assistant.png[Example view of the Watcher assistant for errors in APM app in Kibana] diff --git a/docs/apm/errors.asciidoc b/docs/apm/errors.asciidoc index 689fa1fffa89e..49351ec255858 100644 --- a/docs/apm/errors.asciidoc +++ b/docs/apm/errors.asciidoc @@ -1,7 +1,8 @@ +[role="xpack"] [[errors]] === Errors overview -TIP: {apm-overview-ref-v}/errors.html[Errors] are defined as groups of exceptions with matching exception or log messages. +TIP: {apm-overview-ref-v}/errors.html[Errors] are groups of exceptions with a similar exception or log message. The *Errors* overview provides a high-level view of the error message and culprit, the number of occurrences, and the most recent occurrence. @@ -20,7 +21,7 @@ image::apm/images/apm-error-group.png[Example view of the error group page in th Here, you'll see the error message, culprit, and the number of occurrences over time. Further down, you'll see the Error occurrence table. -This is where you can see the details of a sampled error within this group. +This table shows the details of a sampled error within this group. The error shown is always the most recent to occur. Each error occurrence features a breakdown of the exception, including the stack trace from when the error occurred, @@ -28,19 +29,4 @@ and additional contextual information to help debug the issue. In some cases, you might also see a Transaction sample ID. This feature allows you to make a connection between the errors and transactions, by linking you to the specific transaction where the error occurred. -This allows you to see the whole trace, including which services the request went through. - -[float] -[[errors-alerts-with-watcher]] -==== Error reports with Watcher - -You can use the power of the alerting features with Watcher to get reports on error occurrences. -The Watcher assistant, which is available on the errors overview, can help you set up a watch per service. - -Configure the watch with an occurrences threshold, time interval, and the desired actions, such as email or Slack notifications. -With Watcher, your team can set up reports within minutes. - -Watches are managed separately in the dedicated Watcher UI available in Advanced Settings. - -[role="screenshot"] -image::apm/images/apm-errors-watcher-assistant.png[Example view of the Watcher assistant for errors in APM app in Kibana] \ No newline at end of file +This allows you to see the whole trace, including which services the request went through. diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index 99ba827b0198d..d53adb439f0c8 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -1,6 +1,11 @@ +[role="xpack"] [[filters]] === Filters +++++ +Filter data +++++ + APM provides two different ways you can filter your data within the APM App: * <> @@ -42,7 +47,7 @@ It allows you to view only relevant data, and is especially useful for separatin By default, all environments are displayed. If there are no environment options, you'll see "not defined". Service environments are defined when configuring your APM agents. -It's very important to be consistent when naming environments in your agents. +It's vital to be consistent when naming environments in your agents. See the documentation for each agent you're using to learn how to configure service environments: * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] @@ -62,9 +67,9 @@ but only where they are applicable -- they are typically most useful in their or As an example, if you select a host on the Services overview, then select a transaction group, the host filter will still be applied. -These filters are very useful for quickly and easily removing noise from your data. +These filters are very useful for quickly and easily removing noise from your data. With just a click, you can filter your transactions by the transaction result, -host, container ID, and more. +host, container ID, and more. [role="screenshot"] image::apm/images/local-filter.png[Local filters available in the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index 4a391f1a49672..89ce0be1499c5 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -1,22 +1,45 @@ [role="xpack"] [[apm-getting-started]] -== Getting Started +== Get started with the APM app -If you have not already installed and configured Elastic APM, -the *Setup Instructions* will get you started. +++++ +Get started +++++ -[role="screenshot"] -image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana] +Elastic APM captures different types of information from within instrumented applications: +* *Spans* contain information about the execution of a specific code path. +They measure from the start to end of an activity, +and they can have a parent/child relationship with other spans. +* *Transactions* are a special kind of span; +they are the first span for a particular service and have extra metadata associated with them. +As an example, a transaction could be a request to your server, a batch job, or a custom transaction type. +*Traces* link together related transactions to show an end-to-end performance of how a request was served and which services were part of it. +* *Errors* contain information about the original exception that occurred or about a log created when the exception occurred. -Index patterns tell Kibana which Elasticsearch indices you want to explore. -An APM index pattern is necessary for certain features in the APM app, like the query bar. -To set up the correct index pattern, -simply click *Load Kibana objects* at the bottom of the Setup Instructions. +Curated charts and tables display the different types of APM data, which allows you to compare and debug your applications easily. -After you install an Elastic APM agent library in your application, -the application automatically appears in the APM app in {kib}. -No further configuration is required. +* <> +* <> +* <> +* <> +* <> +* <> +* <> -[role="screenshot"] -image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana] +TIP: Want to learn more about the Elastic APM ecosystem? +See the {apm-get-started-ref}/overview.html[APM Overview]. + +include::services.asciidoc[] + +include::traces.asciidoc[] + +include::transactions.asciidoc[] + +include::spans.asciidoc[] + +include::errors.asciidoc[] + +include::metrics.asciidoc[] + +include::service-maps.asciidoc[] diff --git a/docs/apm/how-to-guides.asciidoc b/docs/apm/how-to-guides.asciidoc new file mode 100644 index 0000000000000..9b0efb4f7a359 --- /dev/null +++ b/docs/apm/how-to-guides.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[apm-how-to]] +== How-to guides + +Learn how to perform common APM app tasks. + + +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> + + +include::agent-configuration.asciidoc[] + +include::apm-alerts.asciidoc[] + +include::custom-links.asciidoc[] + +include::error-reports-watcher.asciidoc[] + +include::filters.asciidoc[] + +include::machine-learning.asciidoc[] + +include::advanced-queries.asciidoc[] + +include::deployment-annotations.asciidoc[] \ No newline at end of file diff --git a/docs/apm/images/dynamic-config.svg b/docs/apm/images/dynamic-config.svg new file mode 100644 index 0000000000000..df62a3c84f4b4 --- /dev/null +++ b/docs/apm/images/dynamic-config.svg @@ -0,0 +1 @@ + DynamicDynamic \ No newline at end of file diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index d3f0dc5b7f11f..79190efccdff2 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -4,25 +4,35 @@ [partintro] -- -Elastic Application Performance Monitoring (APM) automatically collects in-depth -performance metrics and errors from inside your applications. - -The **APM** app in {kib} is provided with the basic license. It -enables developers to drill down into the performance data for their applications -and quickly locate the performance bottlenecks. - -* <> -* <> -* <> - -NOTE: For more information about the components of Elastic APM, -see the {apm-get-started-ref}/overview.html[APM Overview]. +The APM app in {kib} is provided with the basic license. +It allows you to monitor your software services and applications in real-time; +visualize detailed performance information on your services, +identify and analyze errors, +and monitor host-level and agent-specific metrics like JVM and Go runtime metrics. + +[float] +[[apm-bottlenecks]] +== Visualizing application bottlenecks + +Having access to application-level insights with just a few clicks can drastically decrease the time you spend +debugging errors, slow response times, and crashes. + +For example, you can see information about response times, requests per minute, and status codes per endpoint. +You can even dive into a specific request sample and get a complete waterfall view of what your application is spending its time on. +You might see that your bottlenecks are in database queries, cache calls, or external requests. +For each incoming request and each application error, +you can also see contextual information such as the request header, user information, +system values, or custom data that you manually attached to the request. -- +include::set-up.asciidoc[] + include::getting-started.asciidoc[] -include::bottlenecks.asciidoc[] +include::how-to-guides.asciidoc[] -include::using-the-apm-ui.asciidoc[] +include::settings.asciidoc[] include::api.asciidoc[] + +include::troubleshooting.asciidoc[] diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc new file mode 100644 index 0000000000000..9d347fc4f1111 --- /dev/null +++ b/docs/apm/machine-learning.asciidoc @@ -0,0 +1,27 @@ +[role="xpack"] +[[machine-learning-integration]] +=== Machine Learning integration + +++++ +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*. + +[role="screenshot"] +image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in APM app in Kibana] + +[float] +[[create-ml-integration]] +=== Create a new machine learning job + +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. + +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/metrics.asciidoc b/docs/apm/metrics.asciidoc index ab394b785ef84..e82a4fbd5c291 100644 --- a/docs/apm/metrics.asciidoc +++ b/docs/apm/metrics.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[metrics]] === Metrics overview @@ -5,7 +6,7 @@ The *Metrics* overview provides agent-specific metrics, which lets you perform more in-depth root cause analysis investigations within the APM app. If you're experiencing a problem with your service, you can use this page to attempt to find the underlying cause. -For example, you might be able to correlate a high number of errors with a long transaction duration, high CPU usage, or a memory leak. +For example, you might be able to correlate a high number of errors with a long transaction duration, high CPU usage, or a memory leak. [role="screenshot"] image::apm/images/apm-metrics.png[Example view of the Metrics overview in APM app in Kibana] @@ -17,19 +18,3 @@ thread count, garbage collection rate, and garbage collection time spent per min [role="screenshot"] image::apm/images/jvm-metrics.png[Example view of the Metrics overview for the Java Agent] - -[[machine-learning-integration]] -=== Machine Learning integration - -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 annotate the graph 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 APM app in Kibana] - -Jobs can be created per transaction type and based on the average response time. -You can manage jobs in the *Machine Learning jobs management*. -It might take some time for results to appear on the graph. - -Machine learning is a platinum feature. For a comparison of the Elastic license levels, -see https://www.elastic.co/subscriptions[the subscription page]. \ No newline at end of file diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index e0d84f33b4dcb..be86b9d522ac5 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -1,37 +1,53 @@ +[role="xpack"] [[service-maps]] === Service maps beta::[] -A service map is a real-time diagram of the interactions occurring in your application’s architecture. -It allows you to easily visualize data flow and high-level statistics, like average transaction duration, -requests per minute, errors per minute, and metrics, allowing you to quickly assess the status of your services. +WARNING: Service map support for Internet Explorer 11 is extremely limited. +Please use Chrome or Firefox if available. -Our beta offering creates two types of service maps: +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. -* Global: All services and connections are shown. -* Service-specific: Selecting a specific service will highlight it's connections. +We currently surface two types of service maps: + +* Global: All services instrumented with APM agents and the connections between them are shown. +* Service-specific: Highlight connections for a selected service. [role="screenshot"] image::apm/images/service-maps.png[Example view of service maps in the APM app in Kibana] +[float] +[[service-maps-how]] +=== How do service maps work? + +Service maps rely on distributed traces to draw connections between services. +As {apm-overview-ref-v}/distributed-tracing.html[distributed tracing] is enabled out-of-the-box for supported technologies, so are service maps. +However, if a service isn't instrumented, +or a `traceparent` header isn't being propagated to it, +distributed tracing will not work, and the connection will not be drawn on the map. + [float] [[visualize-your-architecture]] === Visualize your architecture Select the **Service Map** tab to get started. -By default, all services and connections are shown. -Whether your onboarding a new engineer, or just trying to grasp the big picture, +By default, all instrumented services and connections are shown. +Whether you're onboarding a new engineer, or just trying to grasp the big picture, click around, zoom in and out, and begin to visualize how your services are connected. If there's a specific service that interests you, select that service to highlight its connections. Clicking **Focus map** will refocus the map on that specific service and lock the connection highlighting. -From here, select **Service Details**, or click on the **Transaction** tab to jump to the Transaction overview. +From here, select **Service Details**, or click on the **Transaction** tab to jump to the Transaction overview +for the selected service. You can also use the tabs at the top of the page to easily jump to the **Errors** or **Metrics** overview. -While it's not possible to query in service maps, it is possible to filter by environment. +Filter out your maps by picking the environment from the environment drop-down filter. This can be useful if you have two or more services, in separate environments, but with the same name. -Use the environment drop down to only see the data you're interested in, like `dev` or `production`. +Use the environment drop-down to only see the data you're interested in, like `dev` or `production`. +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] @@ -46,3 +62,18 @@ Nodes appear on the map in one of two shapes: * **Diamond**: Databases, external, and messaging. Interior icons represent the generic type, with specific icons for known entities, like Elasticsearch. Type and subtype are based on `span.type`, and `span.subtype`. + +[float] +[[service-maps-supported]] +=== Supported APM Agents + +Service maps are supported for the following Agent versions: + +[horizontal] +Go Agent:: >= v1.7.0 +Java Agent:: >= v1.13.0 +.NET Agent:: >= v1.3.0 +Node.js Agent:: >= v3.6.0 +Python Agent:: >= v5.5.0 +Ruby Agent:: >= v3.6.0 +Real User Monitoring (RUM) Agent:: >= v4.7.0 diff --git a/docs/apm/services.asciidoc b/docs/apm/services.asciidoc index 9af3e74562dab..395e23c379306 100644 --- a/docs/apm/services.asciidoc +++ b/docs/apm/services.asciidoc @@ -1,9 +1,9 @@ +[role="xpack"] [[services]] === Services overview -The *Services* overview gives you quick insights into the health and general performance of each service. - -You can add services by setting the `service.name` configuration in each of the {apm-agents-ref}[APM agents] you’re instrumenting. +The *Services* overview gives you quick insights into the health and general performance of all of your instrumented services. +Services are sorted by the `service.name` configured in each of the {apm-agents-ref}[APM agents] you’ve installed. [role="screenshot"] image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/set-up.asciidoc b/docs/apm/set-up.asciidoc new file mode 100644 index 0000000000000..c5bf5e13b640b --- /dev/null +++ b/docs/apm/set-up.asciidoc @@ -0,0 +1,35 @@ +[role="xpack"] +[[apm-ui]] +== Set up the APM app + +++++ +Set up +++++ + +APM is available via the navigation sidebar in {Kib}. +If you have not already installed and configured Elastic APM, +the *Setup Instructions* in Kibana will get you started. + +[role="screenshot"] +image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana] + +[float] +[[apm-configure-index-pattern]] +=== Load the index pattern + +Index patterns tell Kibana which Elasticsearch indices you want to explore. +An APM index pattern is necessary for certain features in the APM app, like the query bar. +To set up the correct index pattern, +simply click *Load Kibana objects* at the bottom of the Setup Instructions. + +[role="screenshot"] +image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana] + +To use a custom index pattern, see <>. + +[float] +[[apm-getting-started-next]] +=== Next steps + +No further configuration in the APM app is required. +Install an APM Agent library in your service to begin visualizing and analyzing your data! diff --git a/docs/apm/settings.asciidoc b/docs/apm/settings.asciidoc index 37122fc9c635d..44da63143f63f 100644 --- a/docs/apm/settings.asciidoc +++ b/docs/apm/settings.asciidoc @@ -1,18 +1,23 @@ // Do not link directly to this page. // Link to the anchor in `/docs/settings/apm-settings.asciidoc` instead. +[role="xpack"] [[apm-settings-in-kibana]] -=== APM settings in Kibana +== APM app settings + +++++ +Settings +++++ You do not need to configure any settings to use the APM app. It is enabled by default. [float] [[apm-indices-settings]] -==== APM Indices +=== APM Indices include::./../settings/apm-settings.asciidoc[tag=apm-indices-settings] [float] [[general-apm-settings]] -==== General APM settings +=== General APM settings include::./../settings/apm-settings.asciidoc[tag=general-apm-settings] diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index ef21e1c5333e0..2eed339160fc4 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -1,7 +1,8 @@ +[role="xpack"] [[spans]] === Span timeline -TIP: A {apm-overview-ref-v}/transaction-spans.html[span] is defined as the duration of a single event. +TIP: A {apm-overview-ref-v}/transaction-spans.html[span] is the duration of a single event. Spans are automatically captured by APM agents, and you can also define custom spans. Each span has a type and is defined by a different color in the timeline/waterfall visualization. @@ -28,7 +29,7 @@ Services in a distributed trace are separated by color and listed in the order t [role="screenshot"] image::apm/images/apm-services-trace.png[Example of distributed trace colors in the APM app in Kibana] -Don't forget, a distributed trace includes more than one transaction. +Don't forget; a distributed trace includes more than one transaction. When viewing these distributed traces in the timeline waterfall, you'll see this image:apm/images/transaction-icon.png[APM icon] icon, which indicates the next transaction in the trace. These transactions can be expanded and viewed in detail by clicking on them. diff --git a/docs/apm/traces.asciidoc b/docs/apm/traces.asciidoc index 09d8f52b92840..8eef3d9bed4db 100644 --- a/docs/apm/traces.asciidoc +++ b/docs/apm/traces.asciidoc @@ -1,12 +1,17 @@ +[role="xpack"] [[traces]] === Traces overview +TIP: Traces link together related transactions to show an end-to-end performance of how a request was served +and which services were part of it. +In addition to the Traces overview, you can view your application traces in the <>. + The *Traces* overview displays the entry transaction for all traces in your application. If you're using <>, this view is key to finding the critical paths within your application. Transactions with the same name are grouped together and only shown once in this table. By default, transactions are sorted by _Impact_. -Impact helps show the most used and slowest endpoints in your service - in other words, +Impact helps show the most used and slowest endpoints in your service--in other words, it's the collective amount of pain a specific endpoint is causing your users. If there's a particular endpoint you're worried about, you can click on it to view the <>. @@ -33,4 +38,4 @@ You can use the <> to view a waterfall displa [role="screenshot"] image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana] -TIP: Distributed tracing is supported by all APM agents and there’s no additional configuration needed. \ No newline at end of file +TIP: Distributed tracing is supported by all APM agents, and there's no additional configuration needed. \ No newline at end of file diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 1eb037009efff..2e1022e6d684c 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[transactions]] === Transaction overview @@ -56,20 +57,6 @@ For further details, including troubleshooting and custom implementation instruc refer to the documentation for each {apm-agents-ref}[APM Agent] you've implemented. ==== -[[transactions-annotations]] -==== Transaction annotations - -For enhanced visibility into your deployments, we offer deployment annotations on all transaction charts. -This feature automatically tags new deployments, so you can easily see if your deploy has increased response times -for an end-user, or if the memory/CPU footprint of your application has increased. -Being able to quickly identify bad deployments enables you to rollback and fix issues without causing costly outages. - -Deployment annotations are automatically enabled, and appear when the `service.version` of your app changes. - -[role="screenshot"] -image::apm/images/apm-transaction-annotation.png[Example view of transactions annotation in the APM app in Kibana] - - [[rum-transaction-overview]] ==== RUM Transaction overview @@ -82,7 +69,7 @@ image::apm/images/apm-geo-ui.jpg[average page load duration distribution] This data is available due to the geo-ip and user agent pipelines being enabled by default, which allows for the capture of geo-location and user agent data. These visualizations make it easy for you to visualize performance information about your -end users' experience based on their location. +end-users' experience based on their location. [[transaction-details]] ==== Transaction details @@ -103,7 +90,7 @@ The number of requests per bucket is displayed when hovering over the graph, and [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] -This graph shows a typical distribution, and indicates most of our requests were served quickly - awesome! +This graph shows a typical distribution, and indicates most of our requests were served quickly--awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. When you select one of these buckets, diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index c6174e1786c78..eb4fb790afd7f 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -1,19 +1,24 @@ -[[troubleshooting]] -=== Troubleshooting common problems +[[troubleshooting]] +== Troubleshoot common problems + +++++ +Troubleshooting +++++ If you have something to add to this section, please consider creating a pull request with your proposed changes at https://github.com/elastic/kibana. -Also check out the https://discuss.elastic.co/c/apm[APM discussion forum]. +Also, check out the https://discuss.elastic.co/c/apm[APM discussion forum]. +[float] [[no-apm-data-found]] -==== No APM data found +=== No APM data found This section can help with any of the following: * Data isn't displaying in the APM app -* You're seeing a message like "No Services Found", -* You're seeing errors like "Fielddata is disabled on text fields by default..." +* You see a message like "No Services Found", +* You see errors like "Fielddata is disabled on text fields by default..." There are a number of factors that could be at play here. One important thing to double-check first is your index template. @@ -52,12 +57,13 @@ you can customize the indices that the APM app uses to display data. Navigate to *APM* > *Settings* > *Indices*, and change all `apm_oss.*Pattern` values to include the new index pattern. For example: `customIndexName-*`. -==== Unknown route +[float] +=== Unknown route The {apm-app-ref}/transactions.html[transaction overview] will only display helpful information when the transactions in your services are named correctly. If you're seeing "GET unknown route" or "unknown route" in the APM app, -it could be a sign that something isn't working like it should. +it could be a sign that something isn't working as it should. Elastic APM Agents come with built-in support for popular frameworks out-of-the-box. This means, among other things, that the Agent will try to automatically name HTTP requests. @@ -71,7 +77,8 @@ To resolve this, you'll need to head over to the relevant {apm-agents-ref}[Agent Specifically, view the Agent's supported technologies page. You can also use the Agent's public API to manually set a name for the transaction. -==== Fields are not searchable +[float] +=== Fields are not searchable In Elasticsearch, index templates are used to define settings and mappings that determine how fields should be analyzed. The recommended index template file for APM Server is installed by the APM Server packages. @@ -92,7 +99,7 @@ Selecting the `apm-*` index pattern shows a listing of every field defined in th *Ensure a field is searchable* There are two things you can do to if you'd like to ensure a field is searchable: -1. Index your additional data as {apm-overview-ref}/metadata.html[labels] instead. +1. Index your additional data as {apm-overview-ref-v}/metadata.html[labels] instead. These are dynamic by default, which means they will be indexed and become searchable and aggregatable. 2. Use the {apm-server-ref}/configuration-template.html[`append_fields`] feature. As an example, diff --git a/docs/apm/using-the-apm-ui.asciidoc b/docs/apm/using-the-apm-ui.asciidoc deleted file mode 100644 index 904718999069d..0000000000000 --- a/docs/apm/using-the-apm-ui.asciidoc +++ /dev/null @@ -1,51 +0,0 @@ -[role="xpack"] -[[apm-ui]] -== Using APM - -APM is designed to be as intuitive as possible, -but you might come across certain terms or concepts that don’t feel native to you. -Not to worry, we've created this guide to help you get the most out of Elastic APM. - -APM is available via the navigation sidebar in {Kib}. - -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -include::filters.asciidoc[] - -include::services.asciidoc[] - -include::traces.asciidoc[] - -include::transactions.asciidoc[] - -include::spans.asciidoc[] - -include::service-maps.asciidoc[] - -include::errors.asciidoc[] - -include::metrics.asciidoc[] - -include::apm-alerts.asciidoc[] - -include::agent-configuration.asciidoc[] - -include::custom-links.asciidoc[] - -include::advanced-queries.asciidoc[] - -include::settings.asciidoc[] - -include::troubleshooting.asciidoc[] diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md new file mode 100644 index 0000000000000..d1a1ee0905c6e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) + +## IndexPatternField.indexPattern property + +Signature: + +```typescript +indexPattern?: IndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 121ae80734dfd..df0de6ce0e541 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -27,6 +27,7 @@ export declare class Field implements IFieldType | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | | [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) | | any | | +| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | | diff --git a/docs/ingest_manager/index.asciidoc b/docs/ingest_manager/index.asciidoc index 1254f412e14c5..22afa88c919e4 100644 --- a/docs/ingest_manager/index.asciidoc +++ b/docs/ingest_manager/index.asciidoc @@ -113,7 +113,7 @@ Ingest Management enforces an indexing strategy to allow the system to automatic {type}-{dataset}-{namespace} ``` -The `{type}` can be `logs` or `metrics`. The `{namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. +The `{type}` can be `logs` or `metrics`. The `{namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`. Note: More `{type}`s might be added in the future like `apm` and `endpoint`. @@ -126,6 +126,8 @@ This indexing strategy has a few advantages: * Having a global metrics and logs template, allows to create new indices on demand which still follow the convention. This is common in the case of k8s as an example. * Constant keywords allow to narrow down the indices we need to access for querying very efficiently. This is especially relevant in environments which a large number of indices or with indices on slower nodes. +Overall it creates smaller indices in size, makes querying more efficient and allows users to define their own naming parts in namespace and still benefiting from all features that can be built on top of the indexing startegy. + === Ingest Pipeline The ingest pipelines for a specific dataset will have the following naming scheme: diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 91bbef5690fd5..fd53c3aeb3605 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -5,7 +5,10 @@ APM settings ++++ -You do not need to configure any settings to use the APM app. It is enabled by default. +These settings allow the APM app to function, and specify the data that it surfaces. +Unless you've customized your setup, +you do not need to configure any settings to use the APM app. +It is enabled by default. [float] [[apm-indices-settings-kb]] @@ -33,29 +36,29 @@ image::settings/images/apm-settings.png[APM app settings in Kibana] If you'd like to change any of the default values, copy and paste the relevant settings into your `kibana.yml` configuration file. +Changing these settings may disable features of the APM App. -xpack.apm.enabled:: Set to `false` to disabled the APM plugin {kib}. Defaults to -`true`. +xpack.apm.enabled:: Set to `false` to disable the APM app. Defaults to `true`. -xpack.apm.ui.enabled:: Set to `false` to hide the APM plugin {kib} from the menu. Defaults to -`true`. +xpack.apm.ui.enabled:: Set to `false` to hide the APM app from the menu. Defaults to `true`. -xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in APM plugin in Kibana. Defaults to `100`. +xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in the APM app. Defaults to `100`. -xpack.apm.ui.maxTraceItems:: Max number of child items displayed when viewing trace details. Defaults to `1000`. +xpack.apm.ui.maxTraceItems:: Maximum number of child items displayed when viewing trace details. Defaults to `1000`. -apm_oss.indexPattern:: Index pattern is used for integrations with Machine Learning and Kuery Bar. It must match all apm indices. Defaults to `apm-*`. +apm_oss.indexPattern:: The index pattern used for integrations with Machine Learning and Query Bar. +It must match all apm indices. Defaults to `apm-*`. -apm_oss.errorIndices:: Matcher for indices containing error documents. Defaults to `apm-*`. +apm_oss.errorIndices:: Matcher for all {apm-server-ref}/error-indices.html[error indices]. Defaults to `apm-*`. -apm_oss.onboardingIndices:: Matcher for indices containing onboarding documents. Defaults to `apm-*`. +apm_oss.onboardingIndices:: Matcher for all onboarding indices. Defaults to `apm-*`. -apm_oss.spanIndices:: Matcher for indices containing span documents. Defaults to `apm-*`. +apm_oss.spanIndices:: Matcher for all {apm-server-ref}/span-indices.html[span indices]. Defaults to `apm-*`. -apm_oss.transactionIndices:: Matcher for indices containing transaction documents. Defaults to `apm-*`. +apm_oss.transactionIndices:: Matcher for all {apm-server-ref}/transaction-indices.html[transaction indices]. Defaults to `apm-*`. -apm_oss.metricsIndices:: Matcher for indices containing metric documents. Defaults to `apm-*`. +apm_oss.metricsIndices:: Matcher for all {apm-server-ref}/metricset-indices.html[metrics indices]. Defaults to `apm-*`. -apm_oss.sourcemapIndices:: Matcher for indices containing sourcemap documents. Defaults to `apm-*`. +apm_oss.sourcemapIndices:: Matcher for all {apm-server-ref}/sourcemap-indices.html[source map indices]. Defaults to `apm-*`. // end::general-apm-settings[] diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 02c09736e1fa0..2913bf28dd765 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -2,181 +2,46 @@ [[action-types]] == Action and connector types -{kib} provides the following types of actions: +Actions are Kibana services or integrations with third-party systems that run as background tasks on the Kibana server when alert conditions are met. {kib} provides the following types of actions: -* <> -* <> -* <> -* <> -* <> -* <> +[cols="2"] +|=== -This section describes how to configure connectors and actions for each type. +a| <> -[NOTE] -============================================== -Some action types are paid commercial features, while others are free. -For a comparison of the Elastic license levels, -see https://www.elastic.co/subscriptions[the subscription page]. -============================================== - -[float] -[[email-action-type]] -=== Email - -The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. - -[float] -[[email-connector-configuration]] -==== Connector configuration - -Email 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. -Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. -Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. -Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. -Username:: username for 'login' type authentication. -Password:: password for 'login' type authentication. - -[float] -[[email-action-configuration]] -==== Action configuration - -Email actions have the following configuration properties: - -To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. -Subject:: The subject line of the email. -Message:: The message text of the email. Markdown format is supported. - -[float] -[[index-action-type]] -=== Index - -The index action type will index a document into {es}. - -[float] -[[index-connector-configuration]] -==== Connector configuration - -Index 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. -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] -[[index-action-configuration]] -==== Action configuration - -Index actions have the following properties: - -Document:: The document to index in json format. - -[float] -[[pagerduty-action-type]] -=== PagerDuty - -The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. - -[float] -[[pagerduty-connector-configuration]] -==== Connector configuration +| Send email from your server. -PagerDuty connectors have the following configuration properties: +a| <> -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. -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. -Routing Key:: A 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. +| Index data into Elasticsearch. -[float] -[[pagerduty-action-configuration]] -==== Action configuration +a| <> -PagerDuty actions have the following properties: +| Send an event in PagerDuty. -Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). -Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. -Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. -Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. -Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. -Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. -Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. -Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. -Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. +a| <> -For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. +| Add a message to a Kibana log. -[float] -[[server-log-action-type]] -=== Server log +a| <> -This action type writes and entry to the {kib} server log. +| Send a message to a Slack channel or user. -[float] -[[server-log-connector-configuration]] -==== Connector configuration +a| <> -Server log connectors have the following configuration properties: +| Send a request to a web service. +|=== -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] -[[server-log-action-configuration]] -==== Action configuration - -Server log actions have the following properties: - -Message:: The message to log. - -[float] -[[slack-action-type]] -=== Slack - -The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. - -[float] -[[slack-connector-configuration]] -==== Connector configuration - -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] -[[slack-action-configuration]] -==== Action configuration - -Slack actions have the following properties: - -Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. - -[float] -[[webhook-action-type]] -=== Webhook - -The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. - -[float] -[[webhook-connector-configuration]] -==== Connector configuration - -Webhook 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. -URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. -Method:: HTTP request method, either `post`(default) or `put`. -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] -[[webhook-action-configuration]] -==== Action configuration - -Webhook actions have the following properties: +[NOTE] +============================================== +Some action types are paid commercial features, while others are free. +For a comparison of the Elastic subscription levels, +see https://www.elastic.co/subscriptions[the subscription page]. +============================================== -Body:: A json payload sent to the request URL. \ No newline at end of file +include::action-types/email.asciidoc[] +include::action-types/index.asciidoc[] +include::action-types/pagerduty.asciidoc[] +include::action-types/server-log.asciidoc[] +include::action-types/slack.asciidoc[] +include::action-types/webhook.asciidoc[] diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc new file mode 100644 index 0000000000000..be3623dd9e59c --- /dev/null +++ b/docs/user/alerting/action-types/email.asciidoc @@ -0,0 +1,29 @@ +[role="xpack"] +[[email-action-type]] +== Email action type + +The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. + +[float] +[[email-connector-configuration]] +==== Connector configuration + +Email 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. +Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. +Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. +Port:: The port to connect to on the service provider. +Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Username:: username for 'login' type authentication. +Password:: password for 'login' type authentication. + +[float] +[[email-action-configuration]] +==== Action configuration + +Email actions have the following configuration properties: + +To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. +Subject:: The subject line of the email. +Message:: The message text of the email. Markdown format is supported. \ No newline at end of file diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc new file mode 100644 index 0000000000000..75d9e57b1f212 --- /dev/null +++ b/docs/user/alerting/action-types/index.asciidoc @@ -0,0 +1,24 @@ +[role="xpack"] +[[index-action-type]] +== Index action type + +The index action type will index a document into {es}. + +[float] +[[index-connector-configuration]] +==== Connector configuration + +Index 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. +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] +[[index-action-configuration]] +==== Action configuration + +Index actions have the following properties: + +Document:: The document to index in json format. \ No newline at end of file diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc new file mode 100644 index 0000000000000..50a1f31e4a9ae --- /dev/null +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -0,0 +1,33 @@ +[role="xpack"] +[[pagerduty-action-type]] +== PagerDuty action type + +The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. + +[float] +[[pagerduty-connector-configuration]] +==== Connector configuration + +PagerDuty 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. +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. +Routing Key:: A 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. + +[float] +[[pagerduty-action-configuration]] +==== Action configuration + +PagerDuty actions have the following properties: + +Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). +Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. +Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. +Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. +Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. +Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. +Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. + +For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. \ No newline at end of file diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc new file mode 100644 index 0000000000000..4efbdf3bea099 --- /dev/null +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -0,0 +1,21 @@ +[role="xpack"] +[[server-log-action-type]] +== Server log action type + +This action type writes and entry to the {kib} server log. + +[float] +[[server-log-connector-configuration]] +==== Connector configuration + +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] +[[server-log-action-configuration]] +==== Action configuration + +Server log actions have the following properties: + +Message:: The message to log. \ No newline at end of file diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc new file mode 100644 index 0000000000000..a4bacbf162e46 --- /dev/null +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -0,0 +1,22 @@ +[role="xpack"] +[[slack-action-type]] +== Slack action type + +The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. + +[float] +[[slack-connector-configuration]] +==== Connector configuration + +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] +[[slack-action-configuration]] +==== Action configuration + +Slack actions have the following properties: + +Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. \ No newline at end of file diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc new file mode 100644 index 0000000000000..8c211aa83af89 --- /dev/null +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -0,0 +1,26 @@ +[role="xpack"] +[[webhook-action-type]] +== Webhook action type + +The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. + +[float] +[[webhook-connector-configuration]] +==== Connector configuration + +Webhook 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. +URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. +Method:: HTTP request method, either `post`(default) or `put`. +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] +[[webhook-action-configuration]] +==== Action configuration + +Webhook actions have the following properties: + +Body:: A json payload sent to the request URL. \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index f556cf71bf06c..df11f5f03a7de 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -154,10 +154,13 @@ Pre-packaged *alert types* simplify setup, hide the details complex domain-speci [[alerting-setup-prerequisites]] == Setup and prerequisites +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. + If you are using an *on-premises* Elastic Stack deployment with <>: -* TLS must be configured for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. -* In the kibana.yml configuration file, add the <> +* Transport Layer Security (TLS) must be configured for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. [float] [[alerting-security]] diff --git a/docs/user/reporting/chromium-sandbox.asciidoc b/docs/user/reporting/chromium-sandbox.asciidoc index 5d4fbfb153a0b..bfef5b8b86c6b 100644 --- a/docs/user/reporting/chromium-sandbox.asciidoc +++ b/docs/user/reporting/chromium-sandbox.asciidoc @@ -11,12 +11,12 @@ sandboxing techniques differ for each operating system. The Linux sandbox depends on user namespaces, which were introduced with the 3.8 Linux kernel. However, many distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. {reporting} will automatically disable the sandbox when it is running on Debian and CentOS as additional steps are required to enable -unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} logs: -`Enabling the Chromium sandbox provides an additional layer of protection`. +unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} startup logs: +`Chromium sandbox provides an additional layer of protection, but is not supported for your OS. +Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.` -If your kernel is 3.8 or newer, it's -recommended to enable usernamespaces and set `xpack.reporting.capture.browser.chromium.disableSandbox: false` in your -`kibana.yml` to enable the sandbox. +Reporting will automatically enable the Chromium sandbox at startup when a supported OS is detected. However, if your kernel is 3.8 or newer, it's +recommended to set `xpack.reporting.capture.browser.chromium.disableSandbox: false` in your `kibana.yml` to explicitly enable usernamespaces. ==== Docker When running {kib} in a Docker container, all container processes are run within a usernamespace with seccomp-bpf and diff --git a/examples/embeddable_examples/common/index.ts b/examples/embeddable_examples/common/index.ts new file mode 100644 index 0000000000000..726420fb9bdc3 --- /dev/null +++ b/examples/embeddable_examples/common/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { TodoSavedObjectAttributes } from './todo_saved_object_attributes'; diff --git a/examples/embeddable_examples/common/todo_saved_object_attributes.ts b/examples/embeddable_examples/common/todo_saved_object_attributes.ts new file mode 100644 index 0000000000000..21b6df20fea90 --- /dev/null +++ b/examples/embeddable_examples/common/todo_saved_object_attributes.ts @@ -0,0 +1,26 @@ +/* + * 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 { SavedObjectAttributes } from '../../../src/core/types'; + +export interface TodoSavedObjectAttributes extends SavedObjectAttributes { + task: string; + icon?: string; + title?: string; +} diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index c70bc7009ff51..f446e7f31ac8e 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -3,7 +3,7 @@ "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["embeddable_examples"], - "server": false, + "server": true, "ui": true, "requiredPlugins": ["embeddable"], "optionalPlugins": [] diff --git a/examples/embeddable_examples/public/create_sample_data.ts b/examples/embeddable_examples/public/create_sample_data.ts new file mode 100644 index 0000000000000..bd5ade18aa91e --- /dev/null +++ b/examples/embeddable_examples/public/create_sample_data.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClientContract } from 'kibana/public'; +import { TodoSavedObjectAttributes } from '../common'; + +export async function createSampleData(client: SavedObjectsClientContract) { + await client.create( + 'todo', + { + task: 'Take the garbage out', + title: 'Garbage', + icon: 'trash', + }, + { + id: 'sample-todo-saved-object', + overwrite: true, + } + ); +} diff --git a/examples/embeddable_examples/public/index.ts b/examples/embeddable_examples/public/index.ts index 5fcd454b17a5c..4aac63fb52e2b 100644 --- a/examples/embeddable_examples/public/index.ts +++ b/examples/embeddable_examples/public/index.ts @@ -17,7 +17,6 @@ * under the License. */ -import { PluginInitializer } from 'kibana/public'; export { HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddable, @@ -26,18 +25,8 @@ export { export { ListContainer, LIST_CONTAINER } from './list_container'; export { TODO_EMBEDDABLE } from './todo'; -import { - EmbeddableExamplesPlugin, - EmbeddableExamplesSetupDependencies, - EmbeddableExamplesStartDependencies, -} from './plugin'; +import { EmbeddableExamplesPlugin } from './plugin'; export { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container'; export { MULTI_TASK_TODO_EMBEDDABLE } from './multi_task_todo'; - -export const plugin: PluginInitializer< - void, - void, - EmbeddableExamplesSetupDependencies, - EmbeddableExamplesStartDependencies -> = () => new EmbeddableExamplesPlugin(); +export const plugin = () => new EmbeddableExamplesPlugin(); diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 31a3037332dda..75d34d2d6878f 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -21,12 +21,20 @@ import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddabl import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from './hello_world'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoInput, TodoOutput } from './todo'; -import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo'; +import { + MULTI_TASK_TODO_EMBEDDABLE, + MultiTaskTodoEmbeddableFactory, + MultiTaskTodoInput, + MultiTaskTodoOutput, +} from './multi_task_todo'; import { SEARCHABLE_LIST_CONTAINER, SearchableListContainerFactory, } from './searchable_list_container'; import { LIST_CONTAINER, ListContainerFactory } from './list_container'; +import { createSampleData } from './create_sample_data'; +import { TodoRefInput, TodoRefOutput, TODO_REF_EMBEDDABLE } from './todo/todo_ref_embeddable'; +import { TodoRefEmbeddableFactory } from './todo/todo_ref_embeddable_factory'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; @@ -36,9 +44,18 @@ export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; } +export interface EmbeddableExamplesStart { + createSampleData: () => Promise; +} + export class EmbeddableExamplesPlugin implements - Plugin { + Plugin< + void, + EmbeddableExamplesStart, + EmbeddableExamplesSetupDependencies, + EmbeddableExamplesStartDependencies + > { public setup( core: CoreSetup, deps: EmbeddableExamplesSetupDependencies @@ -48,7 +65,7 @@ export class EmbeddableExamplesPlugin new HelloWorldEmbeddableFactory() ); - deps.embeddable.registerEmbeddableFactory( + deps.embeddable.registerEmbeddableFactory( MULTI_TASK_TODO_EMBEDDABLE, new MultiTaskTodoEmbeddableFactory() ); @@ -73,9 +90,21 @@ export class EmbeddableExamplesPlugin openModal: (await core.getStartServices())[0].overlays.openModal, })) ); + + deps.embeddable.registerEmbeddableFactory( + TODO_REF_EMBEDDABLE, + new TodoRefEmbeddableFactory(async () => ({ + savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) + ); } - public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {} + public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) { + return { + createSampleData: () => createSampleData(core.savedObjects.client), + }; + } public stop() {} } diff --git a/examples/embeddable_examples/public/todo/README.md b/examples/embeddable_examples/public/todo/README.md new file mode 100644 index 0000000000000..e782511f093b3 --- /dev/null +++ b/examples/embeddable_examples/public/todo/README.md @@ -0,0 +1,43 @@ +There are two examples in here: + - TodoEmbeddable + - TodoRefEmbeddable + + # TodoEmbeddable + + The first example you should review is the HelloWorldEmbeddable. That is as basic an embeddable as you can get. + This embeddable is the next step up - an embeddable that renders dynamic input data. The data is simple: + - a required task string + - an optional title + - an optional icon string + - an optional search string + +It also has output data, which is `hasMatch` - whether or not the search string has matched any input data. + +`hasMatch` is a better fit for output data than input data, because it's state that is _derived_ from input data. + +For example, if it was input data, you could create a TodoEmbeddable with input like this: + +```ts + todoEmbeddableFactory.create({ task: 'take out the garabage', search: 'garbage', hasMatch: false }); +``` + +That's wrong because there is actually a match from the search string inside the task. + +The TodoEmbeddable component itself doesn't do anything with the `hasMatch` variable other than set it, but +if you check out `SearchableListContainer`, you can see an example where this output data is being used. + +## TodoRefEmbeddable + +This is an example of an embeddable based off of a saved object. The input is just the `savedObjectId` and +the `search` string. It has even more output parameters, and this time, it does read it's own output parameters in +order to calculate `hasMatch`. + +Output: +```ts +{ + hasMatch: boolean, + savedAttributes?: TodoSavedAttributes +} +``` + +`savedAttributes` is optional because it's possible a TodoSavedObject could not be found with the given savedObjectId. diff --git a/examples/embeddable_examples/public/todo/todo_ref_component.tsx b/examples/embeddable_examples/public/todo/todo_ref_component.tsx new file mode 100644 index 0000000000000..8e0a17be1ec72 --- /dev/null +++ b/examples/embeddable_examples/public/todo/todo_ref_component.tsx @@ -0,0 +1,86 @@ +/* + * 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 { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; + +import { EuiText } from '@elastic/eui'; +import { EuiAvatar } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public'; +import { TodoRefInput, TodoRefOutput, TodoRefEmbeddable } from './todo_ref_embeddable'; + +interface Props { + embeddable: TodoRefEmbeddable; + input: TodoRefInput; + output: TodoRefOutput; +} + +function wrapSearchTerms(task?: string, search?: string) { + if (!search) return task; + if (!task) return task; + const parts = task.split(new RegExp(`(${search})`, 'g')); + return parts.map((part, i) => + part === search ? ( + + {part} + + ) : ( + part + ) + ); +} + +export function TodoRefEmbeddableComponentInner({ + input: { search }, + output: { savedAttributes }, +}: Props) { + const icon = savedAttributes?.icon; + const title = savedAttributes?.title; + const task = savedAttributes?.task; + return ( + + + {icon ? ( + + ) : ( + + )} + + + + + +

{wrapSearchTerms(title || '', search)}

+
+
+ + {wrapSearchTerms(task, search)} + +
+
+
+ ); +} + +export const TodoRefEmbeddableComponent = withEmbeddableSubscription< + TodoRefInput, + TodoRefOutput, + TodoRefEmbeddable +>(TodoRefEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/todo/todo_ref_embeddable.tsx b/examples/embeddable_examples/public/todo/todo_ref_embeddable.tsx new file mode 100644 index 0000000000000..da2dfb2c1a290 --- /dev/null +++ b/examples/embeddable_examples/public/todo/todo_ref_embeddable.tsx @@ -0,0 +1,153 @@ +/* + * 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 { Subscription } from 'rxjs'; +import { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { + Embeddable, + IContainer, + EmbeddableOutput, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { TodoRefEmbeddableComponent } from './todo_ref_component'; + +// Notice this is not the same value as the 'todo' saved object type. Many of our +// cases in prod today use the same value, but this is unnecessary. +export const TODO_REF_EMBEDDABLE = 'TODO_REF_EMBEDDABLE'; + +export interface TodoRefInput extends SavedObjectEmbeddableInput { + /** + * Optional search string which will be used to highlight search terms as + * well as calculate `output.hasMatch`. + */ + search?: string; +} + +export interface TodoRefOutput extends EmbeddableOutput { + /** + * Should be true if input.search is defined and the task or title contain + * search as a substring. + */ + hasMatch: boolean; + /** + * Will contain the saved object attributes of the Todo Saved Object that matches + * `input.savedObjectId`. If the id is invalid, this may be undefined. + */ + savedAttributes?: TodoSavedObjectAttributes; +} + +/** + * Returns whether any attributes contain the search string. If search is empty, true is returned. If + * there are no savedAttributes, false is returned. + * @param search - the search string + * @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId` + */ +function getHasMatch(search?: string, savedAttributes?: TodoSavedObjectAttributes): boolean { + if (!search) return true; + if (!savedAttributes) return false; + return Boolean( + (savedAttributes.task && savedAttributes.task.match(search)) || + (savedAttributes.title && savedAttributes.title.match(search)) + ); +} + +/** + * This is an example of an embeddable that is backed by a saved object. It's essentially the + * same as `TodoEmbeddable` but that is "by value", while this is "by reference". + */ +export class TodoRefEmbeddable extends Embeddable { + public readonly type = TODO_REF_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private savedObjectsClient: SavedObjectsClientContract; + private savedObjectId?: string; + + constructor( + initialInput: TodoRefInput, + { + parent, + savedObjectsClient, + }: { + parent?: IContainer; + savedObjectsClient: SavedObjectsClientContract; + } + ) { + super(initialInput, { hasMatch: false }, parent); + this.savedObjectsClient = savedObjectsClient; + + this.subscription = this.getInput$().subscribe(async () => { + // There is a little more work today for this embeddable because it has + // more output it needs to update in response to input state changes. + let savedAttributes: TodoSavedObjectAttributes | undefined; + + // Since this is an expensive task, we save a local copy of the previous + // savedObjectId locally and only retrieve the new saved object if the id + // actually changed. + if (this.savedObjectId !== this.input.savedObjectId) { + this.savedObjectId = this.input.savedObjectId; + const todoSavedObject = await this.savedObjectsClient.get( + 'todo', + this.input.savedObjectId + ); + savedAttributes = todoSavedObject?.attributes; + } + + // The search string might have changed as well so we need to make sure we recalculate + // hasMatch. + this.updateOutput({ + hasMatch: getHasMatch(this.input.search, savedAttributes), + savedAttributes, + }); + }); + } + + public render(node: HTMLElement) { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(, node); + } + + /** + * Lets re-sync our saved object to make sure it's up to date! + */ + public async reload() { + this.savedObjectId = this.input.savedObjectId; + const todoSavedObject = await this.savedObjectsClient.get( + 'todo', + this.input.savedObjectId + ); + const savedAttributes = todoSavedObject?.attributes; + this.updateOutput({ + hasMatch: getHasMatch(this.input.search, savedAttributes), + savedAttributes, + }); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/examples/embeddable_examples/public/todo/todo_ref_embeddable_factory.tsx b/examples/embeddable_examples/public/todo/todo_ref_embeddable_factory.tsx new file mode 100644 index 0000000000000..e585ddd89674f --- /dev/null +++ b/examples/embeddable_examples/public/todo/todo_ref_embeddable_factory.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 { i18n } from '@kbn/i18n'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common'; +import { + IContainer, + EmbeddableStart, + ErrorEmbeddable, + EmbeddableFactoryDefinition, +} from '../../../../src/plugins/embeddable/public'; +import { + TodoRefEmbeddable, + TODO_REF_EMBEDDABLE, + TodoRefInput, + TodoRefOutput, +} from './todo_ref_embeddable'; + +interface StartServices { + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + savedObjectsClient: SavedObjectsClientContract; +} + +export class TodoRefEmbeddableFactory + implements + EmbeddableFactoryDefinition< + TodoRefInput, + TodoRefOutput, + TodoRefEmbeddable, + TodoSavedObjectAttributes + > { + public readonly type = TODO_REF_EMBEDDABLE; + public readonly savedObjectMetaData = { + name: 'Todo', + includeFields: ['task', 'icon', 'title'], + type: 'todo', + getIconForSavedObject: () => 'pencil', + }; + + constructor(private getStartServices: () => Promise) {} + + public async isEditable() { + return true; + } + + public createFromSavedObject = ( + savedObjectId: string, + input: Partial & { id: string }, + parent?: IContainer + ): Promise => { + return this.create({ ...input, savedObjectId }, parent); + }; + + public async create(input: TodoRefInput, parent?: IContainer) { + const { savedObjectsClient } = await this.getStartServices(); + return new TodoRefEmbeddable(input, { + parent, + savedObjectsClient, + }); + } + + public getDisplayName() { + return i18n.translate('embeddableExamples.todo.displayName', { + defaultMessage: 'Todo (by reference)', + }); + } +} diff --git a/examples/embeddable_examples/server/index.ts b/examples/embeddable_examples/server/index.ts new file mode 100644 index 0000000000000..9ddc3bc2cf715 --- /dev/null +++ b/examples/embeddable_examples/server/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/server'; + +import { EmbeddableExamplesPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new EmbeddableExamplesPlugin(); diff --git a/examples/embeddable_examples/server/plugin.ts b/examples/embeddable_examples/server/plugin.ts new file mode 100644 index 0000000000000..d956b834d0d3c --- /dev/null +++ b/examples/embeddable_examples/server/plugin.ts @@ -0,0 +1,31 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { todoSavedObject } from './todo_saved_object'; + +export class EmbeddableExamplesPlugin implements Plugin { + public setup(core: CoreSetup) { + core.savedObjects.registerType(todoSavedObject); + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/examples/embeddable_examples/server/todo_saved_object.ts b/examples/embeddable_examples/server/todo_saved_object.ts new file mode 100644 index 0000000000000..0f67c53cfa3e1 --- /dev/null +++ b/examples/embeddable_examples/server/todo_saved_object.ts @@ -0,0 +1,40 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; + +export const todoSavedObject: SavedObjectsType = { + name: 'todo', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + title: { + type: 'keyword', + }, + task: { + type: 'text', + }, + icon: { + type: 'keyword', + }, + }, + }, + migrations: {}, +}; diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 091130487791b..7fa03739119b4 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -6,6 +6,7 @@ }, "include": [ "index.ts", + "common/**/*.ts", "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", diff --git a/examples/embeddable_explorer/public/plugin.tsx b/examples/embeddable_explorer/public/plugin.tsx index 7c75b108d9912..bba1b1748e207 100644 --- a/examples/embeddable_explorer/public/plugin.tsx +++ b/examples/embeddable_explorer/public/plugin.tsx @@ -18,6 +18,7 @@ */ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { EmbeddableExamplesStart } from 'examples/embeddable_examples/public/plugin'; import { UiActionsService } from '../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; @@ -26,6 +27,7 @@ interface StartDeps { uiActions: UiActionsService; embeddable: EmbeddableStart; inspector: InspectorStart; + embeddableExamples: EmbeddableExamplesStart; } export class EmbeddableExplorerPlugin implements Plugin { @@ -36,6 +38,7 @@ export class EmbeddableExplorerPlugin implements Plugin { private readonly log: Logger; private readonly config$: Observable; - private subscription: Subscription | undefined; + private subscription?: Subscription; private stop$ = new Subject(); private kibanaVersion: string; - createClient: InternalElasticsearchServiceSetup['createClient'] | undefined; - dataClient: InternalElasticsearchServiceSetup['dataClient'] | undefined; - adminClient: InternalElasticsearchServiceSetup['adminClient'] | undefined; + private createClient?: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; + private adminClient?: IClusterClient; constructor(private readonly coreContext: CoreContext) { this.kibanaVersion = coreContext.env.packageInfo.version; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 07ea431dd3a0d..684f50a5666e1 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -201,7 +201,7 @@ export class Server { uiSettings: uiSettingsStart, }; - const pluginsStart = await this.plugins.start(this.coreStart!); + const pluginsStart = await this.plugins.start(this.coreStart); await this.legacy.start({ core: { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index d068e824a3e0a..1221b01657e45 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -140,6 +140,7 @@

{{screenTitle}}

position="'top'" >
-
- -
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js index 3569e9caf4e27..95d6cb6878e53 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js @@ -34,6 +34,8 @@ import { FieldEditor } from 'ui/field_editor'; import { I18nContext } from 'ui/i18n'; import { i18n } from '@kbn/i18n'; +import { IndexHeader } from '../index_header'; + const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; const renderFieldEditor = ( $scope, @@ -49,6 +51,7 @@ const renderFieldEditor = ( render( + - +

diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index c65054f583ef2..69184a513f53a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import './index_header'; +import { IndexHeader } from './index_header'; import './create_edit_field'; import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; @@ -44,6 +44,7 @@ import { createEditIndexPatternPageStateContainer } from './edit_index_pattern_s const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable'; const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable'; const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable'; +const REACT_INDEX_HEADER_DOM_ELEMENT_ID = 'reactIndexHeader'; const TAB_INDEXED_FIELDS = 'indexedFields'; const TAB_SCRIPTED_FIELDS = 'scriptedFields'; @@ -160,6 +161,33 @@ function destroyIndexedFieldsTable() { node && unmountComponentAtNode(node); } +function destroyIndexHeader() { + const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + +function renderIndexHeader($scope, config) { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + , + node + ); + }); +} + function handleTabChange($scope, newTab) { destroyIndexedFieldsTable(); destroySourceFiltersTable(); @@ -391,6 +419,9 @@ uiModules destroyIndexedFieldsTable(); destroyScriptedFieldsTable(); destroySourceFiltersTable(); + destroyIndexHeader(); destroyState(); }); + + renderIndexHeader($scope, config); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.ts similarity index 94% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.ts index 7c288286bd61e..44c05a55b36f9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.ts @@ -17,4 +17,4 @@ * under the License. */ -import './index_header'; +export { IndexHeader } from './index_header'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.html deleted file mode 100644 index d6b91d96f13d3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.html +++ /dev/null @@ -1,59 +0,0 @@ -

-
- -

- - {{indexPattern.title}} -

-
- -
- - - - - -
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.js deleted file mode 100644 index 87bce06c1146c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.js +++ /dev/null @@ -1,40 +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 { uiModules } from 'ui/modules'; -import template from './index_header.html'; -uiModules.get('apps/management').directive('kbnManagementIndexPatternsHeader', function(config) { - return { - restrict: 'E', - template, - replace: true, - scope: { - indexPattern: '=', - setDefault: '&', - refreshFields: '&', - delete: '&', - }, - link: function($scope, $el, attrs) { - $scope.delete = attrs.delete ? $scope.delete : null; - $scope.setDefault = attrs.setDefault ? $scope.setDefault : null; - $scope.refreshFields = attrs.refreshFields ? $scope.refreshFields : null; - config.bindToScope($scope, 'defaultIndex'); - }, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx new file mode 100644 index 0000000000000..866d10ecb0e19 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiToolTip, + EuiFlexItem, + EuiIcon, + EuiTitle, + EuiButtonIcon, +} from '@elastic/eui'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; + +interface IndexHeaderProps { + defaultIndex: string; + indexPattern: IIndexPattern; + setDefault?: () => void; + refreshFields?: () => void; + deleteIndexPattern?: () => void; +} + +const setDefaultAriaLabel = i18n.translate('kbn.management.editIndexPattern.setDefaultAria', { + defaultMessage: 'Set as default index.', +}); + +const setDefaultTooltip = i18n.translate('kbn.management.editIndexPattern.setDefaultTooltip', { + defaultMessage: 'Set as default index.', +}); + +const refreshAriaLabel = i18n.translate('kbn.management.editIndexPattern.refreshAria', { + defaultMessage: 'Reload field list.', +}); + +const refreshTooltip = i18n.translate('kbn.management.editIndexPattern.refreshTooltip', { + defaultMessage: 'Refresh field list.', +}); + +const removeAriaLabel = i18n.translate('kbn.management.editIndexPattern.removeAria', { + defaultMessage: 'Remove index pattern.', +}); + +const removeTooltip = i18n.translate('kbn.management.editIndexPattern.removeTooltip', { + defaultMessage: 'Remove index pattern.', +}); + +export function IndexHeader({ + defaultIndex, + indexPattern, + setDefault, + refreshFields, + deleteIndexPattern, +}: IndexHeaderProps) { + return ( + + + + {defaultIndex === indexPattern.id && ( + + + + )} + + +

{indexPattern.title}

+
+
+
+
+ + + {setDefault && ( + + + + + + )} + + {refreshFields && ( + + + + + + )} + + {deleteIndexPattern && ( + + + + + + )} + + +
+ ); +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/__snapshots__/indexed_fields_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap similarity index 90% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/__snapshots__/indexed_fields_table.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index b38036a0c2bf0..db2a032b1e4d9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/__snapshots__/indexed_fields_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -16,9 +16,10 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` "excluded": false, "format": undefined, "indexPattern": undefined, - "info": undefined, + "info": Array [], "name": "Elastic", "searchable": true, + "type": "name", }, ] } @@ -42,7 +43,7 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` "excluded": false, "format": undefined, "indexPattern": undefined, - "info": undefined, + "info": Array [], "name": "timestamp", "type": "date", }, @@ -68,16 +69,17 @@ exports[`IndexedFieldsTable should render normally 1`] = ` "excluded": false, "format": undefined, "indexPattern": undefined, - "info": undefined, + "info": Array [], "name": "Elastic", "searchable": true, + "type": "name", }, Object { "displayName": "timestamp", "excluded": false, "format": undefined, "indexPattern": undefined, - "info": undefined, + "info": Array [], "name": "timestamp", "type": "date", }, @@ -86,7 +88,7 @@ exports[`IndexedFieldsTable should render normally 1`] = ` "excluded": false, "format": undefined, "indexPattern": undefined, - "info": undefined, + "info": Array [], "name": "conflictingField", "type": "conflict", }, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap index f3aa2c5da4b67..2d51b1722cfb2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -98,19 +98,26 @@ exports[`Table should render normally 1`] = ` Array [ Object { "displayName": "Elastic", - "info": Object {}, + "excluded": false, + "format": "", + "info": Array [], "name": "Elastic", "searchable": true, + "type": "name", }, Object { "displayName": "timestamp", - "info": Object {}, + "excluded": false, + "format": "YYYY-MM-DD", + "info": Array [], "name": "timestamp", "type": "date", }, Object { "displayName": "conflictingField", - "info": Object {}, + "excluded": false, + "format": "", + "info": Array [], "name": "conflictingField", "type": "conflict", }, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.js deleted file mode 100644 index 29e160cf1c182..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.js +++ /dev/null @@ -1,231 +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, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiIcon, EuiInMemoryTable, EuiIconTip } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -export class Table extends PureComponent { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - items: PropTypes.array.isRequired, - editField: PropTypes.func.isRequired, - }; - - renderBooleanTemplate(value, label) { - return value ? : ; - } - - renderFieldName(name, field) { - const { indexPattern } = this.props; - - const infoLabel = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.additionalInfoAriaLabel', - { defaultMessage: 'Additional field information' } - ); - const timeLabel = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.primaryTimeAriaLabel', - { defaultMessage: 'Primary time field' } - ); - const timeContent = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.primaryTimeTooltip', - { defaultMessage: 'This field represents the time that events occurred.' } - ); - - return ( - - {name} - {field.info && field.info.length ? ( - -   - ( -
{info}
- ))} - /> -
- ) : null} - {indexPattern.timeFieldName === name ? ( - -   - - - ) : null} -
- ); - } - - renderFieldType(type, isConflict) { - const label = i18n.translate('kbn.management.editIndexPattern.fields.table.multiTypeAria', { - defaultMessage: 'Multiple type field', - }); - const content = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.multiTypeTooltip', - { - defaultMessage: - 'The type of this field changes across indices. It is unavailable for many analysis functions.', - } - ); - - return ( - - {type} - {isConflict ? ( - -   - - - ) : ( - '' - )} - - ); - } - - render() { - const { items, editField } = this.props; - - const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], - }; - - const columns = [ - { - field: 'displayName', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.nameHeader', { - defaultMessage: 'Name', - }), - dataType: 'string', - sortable: true, - render: (value, field) => { - return this.renderFieldName(value, field); - }, - width: '38%', - 'data-test-subj': 'indexedFieldName', - }, - { - field: 'type', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.typeHeader', { - defaultMessage: 'Type', - }), - dataType: 'string', - sortable: true, - render: value => { - return this.renderFieldType(value, value === 'conflict'); - }, - 'data-test-subj': 'indexedFieldType', - }, - { - field: 'format', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.formatHeader', { - defaultMessage: 'Format', - }), - dataType: 'string', - sortable: true, - }, - { - field: 'searchable', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.searchableHeader', { - defaultMessage: 'Searchable', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.searchableDescription', - { defaultMessage: 'These fields can be used in the filter bar' } - ), - dataType: 'boolean', - sortable: true, - render: value => - this.renderBooleanTemplate( - value, - i18n.translate('kbn.management.editIndexPattern.fields.table.isSearchableAria', { - defaultMessage: 'Is searchable', - }) - ), - }, - { - field: 'aggregatable', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.aggregatableLabel', { - defaultMessage: 'Aggregatable', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.aggregatableDescription', - { defaultMessage: 'These fields can be used in visualization aggregations' } - ), - dataType: 'boolean', - sortable: true, - render: value => - this.renderBooleanTemplate( - value, - i18n.translate('kbn.management.editIndexPattern.fields.table.isAggregatableAria', { - defaultMessage: 'Is aggregatable', - }) - ), - }, - { - field: 'excluded', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.excludedLabel', { - defaultMessage: 'Excluded', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.excludedDescription', - { defaultMessage: 'Fields that are excluded from _source when it is fetched' } - ), - dataType: 'boolean', - sortable: true, - render: value => - this.renderBooleanTemplate( - value, - i18n.translate('kbn.management.editIndexPattern.fields.table.isExcludedAria', { - defaultMessage: 'Is excluded', - }) - ), - }, - { - name: '', - actions: [ - { - name: i18n.translate('kbn.management.editIndexPattern.fields.table.editLabel', { - defaultMessage: 'Edit', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.editDescription', - { defaultMessage: 'Edit' } - ), - icon: 'pencil', - onClick: editField, - type: 'icon', - 'data-test-subj': 'editFieldFormat', - }, - ], - width: '40px', - }, - ]; - - return ( - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx similarity index 66% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/table.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index 4fd9ef7485bdf..d0479a9a9e032 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -19,31 +19,53 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { Table } from '../table'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; +import { IndexedFieldItem } from '../../types'; +import { Table } from './table'; const indexPattern = { timeFieldName: 'timestamp', -}; - -const items = [ - { name: 'Elastic', displayName: 'Elastic', searchable: true, info: {} }, - { name: 'timestamp', displayName: 'timestamp', type: 'date', info: {} }, - { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict', info: {} }, +} as IIndexPattern; + +const items: IndexedFieldItem[] = [ + { + name: 'Elastic', + displayName: 'Elastic', + searchable: true, + info: [], + type: 'name', + excluded: false, + format: '', + }, + { + name: 'timestamp', + displayName: 'timestamp', + type: 'date', + info: [], + excluded: false, + format: 'YYYY-MM-DD', + }, + { + name: 'conflictingField', + displayName: 'conflictingField', + type: 'conflict', + info: [], + excluded: false, + format: '', + }, ]; describe('Table', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( + test('should render normally', () => { + const component = shallow( {}} /> ); expect(component).toMatchSnapshot(); }); - it('should render normal field name', async () => { - const component = shallowWithI18nProvider( + test('should render normal field name', () => { + const component = shallow(
{}} /> ); @@ -51,8 +73,8 @@ describe('Table', () => { expect(tableCell).toMatchSnapshot(); }); - it('should render timestamp field name', async () => { - const component = shallowWithI18nProvider( + test('should render timestamp field name', () => { + const component = shallow(
{}} /> ); @@ -60,8 +82,8 @@ describe('Table', () => { expect(tableCell).toMatchSnapshot(); }); - it('should render the boolean template (true)', async () => { - const component = shallowWithI18nProvider( + test('should render the boolean template (true)', () => { + const component = shallow(
{}} /> ); @@ -69,8 +91,8 @@ describe('Table', () => { expect(tableCell).toMatchSnapshot(); }); - it('should render the boolean template (false)', async () => { - const component = shallowWithI18nProvider( + test('should render the boolean template (false)', () => { + const component = shallow(
{}} /> ); @@ -78,8 +100,8 @@ describe('Table', () => { expect(tableCell).toMatchSnapshot(); }); - it('should render normal type', async () => { - const component = shallowWithI18nProvider( + test('should render normal type', () => { + const component = shallow(
{}} /> ); @@ -87,8 +109,8 @@ describe('Table', () => { expect(tableCell).toMatchSnapshot(); }); - it('should render conflicting type', async () => { - const component = shallowWithI18nProvider( + test('should render conflicting type', () => { + const component = shallow(
{}} /> ); @@ -96,10 +118,10 @@ describe('Table', () => { expect(tableCell).toMatchSnapshot(); }); - it('should allow edits', () => { + test('should allow edits', () => { const editField = jest.fn(); - const component = shallowWithI18nProvider( + const component = shallow(
); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.tsx new file mode 100644 index 0000000000000..aa8e8b8e13a07 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -0,0 +1,281 @@ +/* + * 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, { PureComponent } from 'react'; + +import { EuiIcon, EuiInMemoryTable, EuiIconTip, EuiBasicTableColumn } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; +import { IndexedFieldItem } from '../../types'; + +// localized labels +const additionalInfoAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.additionalInfoAriaLabel', + { defaultMessage: 'Additional field information' } +); + +const primaryTimeAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.primaryTimeAriaLabel', + { defaultMessage: 'Primary time field' } +); + +const primaryTimeTooltip = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.primaryTimeTooltip', + { defaultMessage: 'This field represents the time that events occurred.' } +); + +const multiTypeAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.multiTypeAria', + { + defaultMessage: 'Multiple type field', + } +); + +const multiTypeTooltip = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.multiTypeTooltip', + { + defaultMessage: + 'The type of this field changes across indices. It is unavailable for many analysis functions.', + } +); + +const nameHeader = i18n.translate('kbn.management.editIndexPattern.fields.table.nameHeader', { + defaultMessage: 'Name', +}); + +const typeHeader = i18n.translate('kbn.management.editIndexPattern.fields.table.typeHeader', { + defaultMessage: 'Type', +}); + +const formatHeader = i18n.translate('kbn.management.editIndexPattern.fields.table.formatHeader', { + defaultMessage: 'Format', +}); + +const searchableHeader = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.searchableHeader', + { + defaultMessage: 'Searchable', + } +); + +const searchableDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.searchableDescription', + { defaultMessage: 'These fields can be used in the filter bar' } +); + +const isSearchableAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.isSearchableAria', + { + defaultMessage: 'Is searchable', + } +); + +const aggregatableLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.aggregatableLabel', + { + defaultMessage: 'Aggregatable', + } +); + +const aggregatableDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.aggregatableDescription', + { defaultMessage: 'These fields can be used in visualization aggregations' } +); + +const isAggregatableAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.isAggregatableAria', + { + defaultMessage: 'Is aggregatable', + } +); + +const excludedLabel = i18n.translate('kbn.management.editIndexPattern.fields.table.excludedLabel', { + defaultMessage: 'Excluded', +}); + +const excludedDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.excludedDescription', + { defaultMessage: 'Fields that are excluded from _source when it is fetched' } +); + +const isExcludedAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.isExcludedAria', + { + defaultMessage: 'Is excluded', + } +); + +const editLabel = i18n.translate('kbn.management.editIndexPattern.fields.table.editLabel', { + defaultMessage: 'Edit', +}); + +const editDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.editDescription', + { defaultMessage: 'Edit' } +); + +interface IndexedFieldProps { + indexPattern: IIndexPattern; + items: IndexedFieldItem[]; + editField: (field: IndexedFieldItem) => void; +} + +export class Table extends PureComponent { + renderBooleanTemplate(value: string, arialLabel: string) { + return value ? : ; + } + + renderFieldName(name: string, field: IndexedFieldItem) { + const { indexPattern } = this.props; + + return ( + + {name} + {field.info && field.info.length ? ( + +   + ( +
{info}
+ ))} + /> +
+ ) : null} + {indexPattern.timeFieldName === name ? ( + +   + + + ) : null} +
+ ); + } + + renderFieldType(type: string, isConflict: boolean) { + return ( + + {type} + {isConflict ? ( + +   + + + ) : ( + '' + )} + + ); + } + + render() { + const { items, editField } = this.props; + + const pagination = { + initialPageSize: 10, + pageSizeOptions: [5, 10, 25, 50], + }; + + const columns: Array> = [ + { + field: 'displayName', + name: nameHeader, + dataType: 'string', + sortable: true, + render: (value: string, field: IndexedFieldItem) => { + return this.renderFieldName(value, field); + }, + width: '38%', + 'data-test-subj': 'indexedFieldName', + }, + { + field: 'type', + name: typeHeader, + dataType: 'string', + sortable: true, + render: (value: string) => { + return this.renderFieldType(value, value === 'conflict'); + }, + 'data-test-subj': 'indexedFieldType', + }, + { + field: 'format', + name: formatHeader, + dataType: 'string', + sortable: true, + }, + { + field: 'searchable', + name: searchableHeader, + description: searchableDescription, + dataType: 'boolean', + sortable: true, + render: (value: string) => this.renderBooleanTemplate(value, isSearchableAriaLabel), + }, + { + field: 'aggregatable', + name: aggregatableLabel, + description: aggregatableDescription, + dataType: 'boolean', + sortable: true, + render: (value: string) => this.renderBooleanTemplate(value, isAggregatableAriaLabel), + }, + { + field: 'excluded', + name: excludedLabel, + description: excludedDescription, + dataType: 'boolean', + sortable: true, + render: (value: string) => this.renderBooleanTemplate(value, isExcludedAriaLabel), + }, + { + name: '', + actions: [ + { + name: editLabel, + description: editDescription, + icon: 'pencil', + onClick: editField, + type: 'icon', + 'data-test-subj': 'editFieldFormat', + }, + ], + width: '40px', + }, + ]; + + return ( + + ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/indexed_fields_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx similarity index 70% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/indexed_fields_table.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 26e271ea477da..f8b78a92e098e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/indexed_fields_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -19,8 +19,8 @@ import React from 'react'; import { shallow } from 'enzyme'; - -import { IndexedFieldsTable } from '../indexed_fields_table'; +import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { IndexedFieldsTable } from './indexed_fields_table'; jest.mock('@elastic/eui', () => ({ EuiFlexGroup: 'eui-flex-group', @@ -29,7 +29,7 @@ jest.mock('@elastic/eui', () => ({ EuiInMemoryTable: 'eui-in-memory-table', })); -jest.mock('../components/table', () => ({ +jest.mock('./components/table', () => ({ // Note: this seems to fix React complaining about non lowercase attributes Table: () => { return 'table'; @@ -37,27 +37,37 @@ jest.mock('../components/table', () => ({ })); const helpers = { - redirectToRoute: () => {}, + redirectToRoute: (obj: any) => {}, + getFieldInfo: () => [], }; const fields = [ - { name: 'Elastic', displayName: 'Elastic', searchable: true }, + { + name: 'Elastic', + displayName: 'Elastic', + searchable: true, + type: 'name', + }, { name: 'timestamp', displayName: 'timestamp', type: 'date' }, { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, -]; +] as IndexPatternField[]; -const indexPattern = { +const indexPattern = ({ getNonScriptedFields: () => fields, -}; +} as unknown) as IIndexPattern; describe('IndexedFieldsTable', () => { - it('should render normally', async () => { + test('should render normally', async () => { const component = shallow( {}} + fieldWildcardMatcher={() => { + return () => false; + }} + indexedFieldTypeFilter="" + fieldFilter="" /> ); @@ -67,13 +77,17 @@ describe('IndexedFieldsTable', () => { expect(component).toMatchSnapshot(); }); - it('should filter based on the query bar', async () => { + test('should filter based on the query bar', async () => { const component = shallow( {}} + fieldWildcardMatcher={() => { + return () => false; + }} + indexedFieldTypeFilter="" + fieldFilter="" /> ); @@ -84,13 +98,17 @@ describe('IndexedFieldsTable', () => { expect(component).toMatchSnapshot(); }); - it('should filter based on the type filter', async () => { + test('should filter based on the type filter', async () => { const component = shallow( {}} + fieldWildcardMatcher={() => { + return () => false; + }} + indexedFieldTypeFilter="" + fieldFilter="" /> ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx similarity index 68% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 074e5784f3dae..7c2bb565615d7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -18,26 +18,33 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { createSelector } from 'reselect'; - +import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; import { Table } from './components/table'; import { getFieldFormat } from './lib'; +import { IndexedFieldItem } from './types'; -export class IndexedFieldsTable extends Component { - static propTypes = { - fields: PropTypes.array.isRequired, - indexPattern: PropTypes.object.isRequired, - fieldFilter: PropTypes.string, - indexedFieldTypeFilter: PropTypes.string, - helpers: PropTypes.shape({ - redirectToRoute: PropTypes.func.isRequired, - getFieldInfo: PropTypes.func, - }), - fieldWildcardMatcher: PropTypes.func.isRequired, +interface IndexedFieldsTableProps { + fields: IndexPatternField[]; + indexPattern: IIndexPattern; + fieldFilter?: string; + indexedFieldTypeFilter?: string; + helpers: { + redirectToRoute: (obj: any) => void; + getFieldInfo: (indexPattern: IIndexPattern, field: string) => string[]; }; + fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; +} + +interface IndexedFieldsTableState { + fields: IndexedFieldItem[]; +} - constructor(props) { +export class IndexedFieldsTable extends Component< + IndexedFieldsTableProps, + IndexedFieldsTableState +> { + constructor(props: IndexedFieldsTableProps) { super(props); this.state = { @@ -45,7 +52,7 @@ export class IndexedFieldsTable extends Component { }; } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: IndexedFieldsTableProps) { if (nextProps.fields !== this.props.fields) { this.setState({ fields: this.mapFields(nextProps.fields), @@ -53,10 +60,11 @@ export class IndexedFieldsTable extends Component { } } - mapFields(fields) { + mapFields(fields: IndexPatternField[]): IndexedFieldItem[] { const { indexPattern, fieldWildcardMatcher, helpers } = this.props; const sourceFilters = - indexPattern.sourceFilters && indexPattern.sourceFilters.map(f => f.value); + indexPattern.sourceFilters && + indexPattern.sourceFilters.map((f: Record) => f.value); const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []); return ( @@ -76,9 +84,10 @@ export class IndexedFieldsTable extends Component { } getFilteredFields = createSelector( - state => state.fields, - (state, props) => props.fieldFilter, - (state, props) => props.indexedFieldTypeFilter, + (state: IndexedFieldsTableState) => state.fields, + (state: IndexedFieldsTableState, props: IndexedFieldsTableProps) => props.fieldFilter, + (state: IndexedFieldsTableState, props: IndexedFieldsTableProps) => + props.indexedFieldTypeFilter, (fields, fieldFilter, indexedFieldTypeFilter) => { if (fieldFilter) { const normalizedFieldFilter = fieldFilter.toLowerCase(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/__jest__/get_field_format.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts similarity index 74% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/__jest__/get_field_format.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts index 7090f70199919..fc7477c074ac2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/__jest__/get_field_format.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts @@ -17,9 +17,10 @@ * under the License. */ -import { getFieldFormat } from '../get_field_format'; +import { IIndexPattern } from '../../../../../../../../../../plugins/data/public'; +import { getFieldFormat } from './get_field_format'; -const indexPattern = { +const indexPattern = ({ fieldFormatMap: { Elastic: { type: { @@ -27,26 +28,26 @@ const indexPattern = { }, }, }, -}; +} as unknown) as IIndexPattern; describe('getFieldFormat', () => { - it('should handle no arguments', () => { + test('should handle no arguments', () => { expect(getFieldFormat()).toEqual(''); }); - it('should handle no field name', () => { + test('should handle no field name', () => { expect(getFieldFormat(indexPattern)).toEqual(''); }); - it('should handle empty name', () => { + test('should handle empty name', () => { expect(getFieldFormat(indexPattern, '')).toEqual(''); }); - it('should handle undefined field name', () => { + test('should handle undefined field name', () => { expect(getFieldFormat(indexPattern, 'none')).toEqual(undefined); }); - it('should retrieve field format', () => { + test('should retrieve field format', () => { expect(getFieldFormat(indexPattern, 'Elastic')).toEqual('string'); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts similarity index 84% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts index 9402694bb1371..1d6f267430f07 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts @@ -18,8 +18,9 @@ */ import { get } from 'lodash'; +import { IIndexPattern } from '../../../../../../../../../../plugins/data/public'; -export function getFieldFormat(indexPattern, fieldName) { +export function getFieldFormat(indexPattern?: IIndexPattern, fieldName?: string): string { return indexPattern && fieldName ? get(indexPattern, ['fieldFormatMap', fieldName, 'type', 'title']) : ''; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/types.ts new file mode 100644 index 0000000000000..f27f4608bf5d5 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/types.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IFieldType } from '../../../../../../../../../plugins/data/public'; + +export interface IndexedFieldItem extends IFieldType { + info: string[]; + excluded: boolean; +} diff --git a/src/legacy/core_plugins/vis_type_timeseries/index.ts b/src/legacy/core_plugins/vis_type_timeseries/index.ts deleted file mode 100644 index 596fd5b581a71..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/index.ts +++ /dev/null @@ -1,45 +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 metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'metrics', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - chartResolution: Joi.number().default(150), - minimumBucketSize: Joi.number().default(10), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default metricsPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_timeseries/package.json b/src/legacy/core_plugins/vis_type_timeseries/package.json deleted file mode 100644 index 6b4874dfe6a68..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "author": "Chris Cowan", - "name": "metrics", - "version": "kibana" -} - diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts b/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts deleted file mode 100644 index 42f116701be51..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts +++ /dev/null @@ -1,33 +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 { MetricsPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts index aa11e0ef41fba..579caa1cb88f6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts @@ -30,6 +30,7 @@ const setupPlugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, charts: npSetup.plugins.charts, + visTypeXy: npSetup.plugins.visTypeXy, }; const startPlugins: Readonly = { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index 2731fb6f5fbe6..ef3f664252856 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -24,6 +24,7 @@ import { PluginInitializerContext, } from 'kibana/public'; +import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; @@ -39,7 +40,6 @@ import { createGoalVisTypeDefinition, } from './vis_type_vislib_vis_types'; import { ChartsPluginSetup } from '../../../../plugins/charts/public'; -import { ConfigSchema as VisTypeXyConfigSchema } from '../../vis_type_xy'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; import { setFormatService, setDataActions } from './services'; @@ -53,6 +53,7 @@ export interface VisTypeVislibPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + visTypeXy?: VisTypeXyPluginSetup; } /** @internal */ @@ -68,7 +69,7 @@ export class VisTypeVislibPlugin implements Plugin { public async setup( core: VisTypeVislibCoreSetup, - { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies + { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies ) { const visualizationDependencies: Readonly = { uiSettings: core.uiSettings, @@ -86,12 +87,8 @@ export class VisTypeVislibPlugin implements Plugin { ]; const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; - const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as - | VisTypeXyConfigSchema['visTypeXy'] - | undefined; - // if visTypeXy plugin is disabled it's config will be undefined - if (!visTypeXy || !visTypeXy.enabled) { + if (!visTypeXy) { const convertedTypes: any[] = []; const convertedFns: any[] = []; diff --git a/src/legacy/core_plugins/vis_type_xy/index.ts b/src/legacy/core_plugins/vis_type_xy/index.ts deleted file mode 100644 index 58d2e425eef40..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/index.ts +++ /dev/null @@ -1,56 +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 '../../types'; - -export interface ConfigSchema { - visTypeXy: { - enabled: boolean; - }; -} - -const visTypeXyPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'visTypeXy', - require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars(server): ConfigSchema { - const config = server.config(); - - return { - visTypeXy: { - enabled: config.get('visTypeXy.enabled') as boolean, - }, - }; - }, - }, - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(false), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default visTypeXyPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_xy/package.json b/src/legacy/core_plugins/vis_type_xy/package.json deleted file mode 100644 index 920f7dcb44e87..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "visTypeXy", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_xy/public/legacy.ts b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts deleted file mode 100644 index 740ceeaac6a7d..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/public/legacy.ts +++ /dev/null @@ -1,40 +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 { npSetup, npStart } from 'ui/new_platform'; -import { PluginInitializerContext } from 'kibana/public'; - -import { plugin } from '.'; -import { VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies } from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, -}; - -const startPlugins: Readonly = { - expressions: npStart.plugins.expressions, - visualizations: npStart.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 33a7fdad065b4..f577a29ce90b9 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -453,6 +453,7 @@ export const npStart = { createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: aggTypesRegistry.start(), + fieldFormats: getFieldFormatsRegistry(mockCoreStart), }); }, types: aggTypesRegistry.start(), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 21b80e827e4c2..80fb837258d4c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -22,6 +22,7 @@ import { IScope } from 'angular'; import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; import { createBrowserHistory } from 'history'; +import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; import { DashboardStart } from '../../../../plugins/dashboard/public'; import { setSetupServices, setStartServices } from './set_services'; import { @@ -93,6 +94,7 @@ export interface PluginsSetup { savedObjectsManagement: SavedObjectsManagementPluginSetup; mapsLegacy: MapsLegacyPluginSetup; indexPatternManagement: IndexPatternManagementSetup; + visTypeXy?: VisTypeXyPluginSetup; } export interface PluginsStart { diff --git a/src/plugins/console/server/lib/spec_definitions/js/ingest.ts b/src/plugins/console/server/lib/spec_definitions/js/ingest.ts index 1182dc075f42f..20dbeda5e0b3d 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/ingest.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/ingest.ts @@ -57,6 +57,49 @@ const bytesProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest-circle-processor.html +const circleProcessorDefinition = { + circle: { + __template: { + field: '', + error_distance: '', + shape_type: '', + }, + field: '', + target_field: '', + error_distance: '', + shape_type: { + __one_of: ['geo_shape', 'shape'], + }, + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/csv-processor.html +const csvProcessorDefinition = { + csv: { + __template: { + field: '', + target_fields: [''], + }, + field: '', + target_fields: [''], + separator: '', + quote: '', + empty_value: '', + trim: { + __one_of: [true, false], + }, + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/convert-processor.html const convertProcessorDefinition = { convert: { @@ -174,6 +217,25 @@ const foreachProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/geoip-processor.html +const geoipProcessorDefinition = { + geoip: { + __template: { + field: '', + }, + field: '', + target_field: '', + database_file: '', + properties: [''], + ignore_missing: { + __one_of: [false, true], + }, + first_only: { + __one_of: [false, true], + }, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/grok-processor.html const grokProcessorDefinition = { grok: { @@ -209,6 +271,37 @@ const gsubProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/htmlstrip-processor.html +const htmlStripProcessorDefinition = { + html_strip: { + __template: { + field: '', + }, + field: '', + target_field: '', + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/inference-processor.html +const inferenceProcessorDefinition = { + inference: { + __template: { + model_id: '', + field_map: {}, + inference_config: {}, + }, + model_id: '', + field_map: {}, + inference_config: {}, + target_field: '', + ...commonPipelineParams, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/join-processor.html const joinProcessorDefinition = { join: { @@ -338,6 +431,18 @@ const setProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest-node-set-security-user-processor.html +const setSecurityUserProcessorDefinition = { + set_security_user: { + __template: { + field: '', + }, + field: '', + properties: [''], + ...commonPipelineParams, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/split-processor.html const splitProcessorDefinition = { split: { @@ -394,10 +499,43 @@ const uppercaseProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/urldecode-processor.html +const urlDecodeProcessorDefinition = { + urldecode: { + __template: { + field: '', + }, + field: '', + target_field: '', + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/user-agent-processor.html +const userAgentProcessorDefinition = { + user_agent: { + __template: { + field: '', + }, + field: '', + target_field: '', + regex_file: '', + properties: [''], + ignore_missing: { + __one_of: [false, true], + }, + }, +}; + const processorDefinition = { __one_of: [ appendProcessorDefinition, bytesProcessorDefinition, + csvProcessorDefinition, + circleProcessorDefinition, convertProcessorDefinition, dateProcessorDefinition, dateIndexNameProcessorDefinition, @@ -406,8 +544,11 @@ const processorDefinition = { dropProcessorDefinition, failProcessorDefinition, foreachProcessorDefinition, + geoipProcessorDefinition, grokProcessorDefinition, gsubProcessorDefinition, + htmlStripProcessorDefinition, + inferenceProcessorDefinition, joinProcessorDefinition, jsonProcessorDefinition, kvProcessorDefinition, @@ -417,10 +558,13 @@ const processorDefinition = { renameProcessorDefinition, scriptProcessorDefinition, setProcessorDefinition, + setSecurityUserProcessorDefinition, splitProcessorDefinition, sortProcessorDefinition, trimProcessorDefinition, uppercaseProcessorDefinition, + urlDecodeProcessorDefinition, + userAgentProcessorDefinition, ], }; diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx index d182deb813e11..fe9a19030602e 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx @@ -28,6 +28,7 @@ import { EmbeddableInput, EmbeddableOutput, EmbeddableStart, + SavedObjectEmbeddableInput, } from '../../embeddable_plugin'; interface Props { @@ -66,7 +67,7 @@ export class ReplacePanelFlyout extends React.Component { }); }; - public onReplacePanel = async (id: string, type: string, name: string) => { + public onReplacePanel = async (savedObjectId: string, type: string, name: string) => { const originalPanels = this.props.container.getInput().panels; const filteredPanels = { ...originalPanels }; @@ -76,7 +77,9 @@ export class ReplacePanelFlyout extends React.Component { const nny = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.y; // add the new view - const newObj = await this.props.container.addSavedObjectEmbeddable(type, id); + const newObj = await this.props.container.addNewEmbeddable(type, { + savedObjectId, + }); const finalPanels = _.cloneDeep(this.props.container.getInput().panels); (finalPanels[newObj.id] as DashboardPanelState).gridData.w = nnw; diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index babd1fac274eb..283fe9f0a83a4 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -53,6 +53,7 @@ import { isErrorEmbeddable, openAddPanelFlyout, ViewMode, + SavedObjectEmbeddableInput, ContainerOutput, } from '../../../embeddable/public'; import { NavAction, SavedDashboardPanel } from '../types'; @@ -394,7 +395,7 @@ export class DashboardAppController { if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) { const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]; const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID]; - container.addSavedObjectEmbeddable(type, id); + container.addNewEmbeddable(type, { savedObjectId: id }); removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE); removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID); } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 7f3a2913daac3..50089f1f061f4 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -55,7 +55,7 @@ export interface DashboardContainerInput extends ContainerInput { description?: string; isFullScreenMode: boolean; panels: { - [panelId: string]: DashboardPanelState; + [panelId: string]: DashboardPanelState; }; isEmptyState?: boolean; } diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 3df305b0d7f1b..6d0221cb10e8b 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; export type PanelId = string; export type SavedObjectId = string; @@ -28,7 +29,8 @@ export interface GridData { i: string; } -export interface DashboardPanelState - extends PanelState { +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { readonly gridData: GridData; } diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts index 447563bbfbcfa..25ce203332422 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts @@ -23,11 +23,6 @@ import { } from './embeddable_saved_object_converters'; import { SavedDashboardPanel } from '../../types'; import { DashboardPanelState } from '../embeddable'; -import { EmbeddableInput } from 'src/plugins/embeddable/public'; - -interface CustomInput extends EmbeddableInput { - something: string; -} test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { @@ -58,8 +53,8 @@ test('convertSavedDashboardPanelToPanelState', () => { explicitInput: { something: 'hi!', id: '123', + savedObjectId: 'savedObjectId', }, - savedObjectId: 'savedObjectId', type: 'search', }); }); @@ -86,7 +81,7 @@ test('convertSavedDashboardPanelToPanelState does not include undefined id', () }); test('convertPanelStateToSavedDashboardPanel', () => { - const dashboardPanel: DashboardPanelState = { + const dashboardPanel: DashboardPanelState = { gridData: { x: 0, y: 0, @@ -94,10 +89,10 @@ test('convertPanelStateToSavedDashboardPanel', () => { w: 15, i: '123', }, - savedObjectId: 'savedObjectId', explicitInput: { something: 'hi!', id: '123', + savedObjectId: 'savedObjectId', }, type: 'search', }; @@ -121,7 +116,7 @@ test('convertPanelStateToSavedDashboardPanel', () => { }); test('convertPanelStateToSavedDashboardPanel will not add an undefined id when not needed', () => { - const dashboardPanel: DashboardPanelState = { + const dashboardPanel: DashboardPanelState = { gridData: { x: 0, y: 0, diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts b/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts index 01cd55df0d8e9..b19ef31ccb9ac 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts @@ -19,6 +19,7 @@ import { omit } from 'lodash'; import { SavedDashboardPanel } from '../../types'; import { DashboardPanelState } from '../embeddable'; +import { SavedObjectEmbeddableInput } from '../../embeddable_plugin'; export function convertSavedDashboardPanelToPanelState( savedDashboardPanel: SavedDashboardPanel @@ -26,9 +27,9 @@ export function convertSavedDashboardPanelToPanelState( return { type: savedDashboardPanel.type, gridData: savedDashboardPanel.gridData, - ...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }), explicitInput: { id: savedDashboardPanel.panelIndex, + ...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }), ...(savedDashboardPanel.title !== undefined && { title: savedDashboardPanel.title }), ...savedDashboardPanel.embeddableConfig, }, @@ -42,13 +43,14 @@ export function convertPanelStateToSavedDashboardPanel( const customTitle: string | undefined = panelState.explicitInput.title ? (panelState.explicitInput.title as string) : undefined; + const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId; return { version, type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, - embeddableConfig: omit(panelState.explicitInput, 'id'), + embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), ...(customTitle && { title: customTitle }), - ...(panelState.savedObjectId !== undefined && { id: panelState.savedObjectId }), + ...(savedObjectId !== undefined && { id: savedObjectId }), }; } diff --git a/src/plugins/data/public/index_patterns/fields/field.ts b/src/plugins/data/public/index_patterns/fields/field.ts index 6370dcdf2db6f..0fb92393d56f7 100644 --- a/src/plugins/data/public/index_patterns/fields/field.ts +++ b/src/plugins/data/public/index_patterns/fields/field.ts @@ -44,6 +44,7 @@ export class Field implements IFieldType { scripted?: boolean; subType?: IFieldSubType; displayName?: string; + indexPattern?: IndexPattern; format: any; $$spec: FieldSpec; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 2ebe377b3b32f..1723545b32522 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -155,7 +155,7 @@ export class DataPublicPlugin implements Plugin { let indexPattern: IndexPattern; let typesRegistry: AggTypesRegistryStart; + const fieldFormats = fieldFormatsServiceMock.createStartContract(); beforeEach(() => { jest.restoreAllMocks(); @@ -40,7 +42,7 @@ describe('AggConfig', () => { describe('#toDsl', () => { it('calls #write()', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { enabled: true, type: 'date_histogram', @@ -55,7 +57,7 @@ describe('AggConfig', () => { }); it('uses the type name as the agg name', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { enabled: true, type: 'date_histogram', @@ -70,7 +72,7 @@ describe('AggConfig', () => { }); it('uses the params from #write() output as the agg params', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { enabled: true, type: 'date_histogram', @@ -100,7 +102,7 @@ describe('AggConfig', () => { params: {}, }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const histoConfig = ac.byName('date_histogram')[0]; const avgConfig = ac.byName('avg')[0]; @@ -210,8 +212,8 @@ describe('AggConfig', () => { testsIdentical.forEach((configState, index) => { it(`identical aggregations (${index})`, () => { - const ac1 = new AggConfigs(indexPattern, configState, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, configState, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, configState, { typesRegistry, fieldFormats }); + const ac2 = new AggConfigs(indexPattern, configState, { typesRegistry, fieldFormats }); expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); }); }); @@ -251,8 +253,8 @@ describe('AggConfig', () => { testsIdenticalDifferentOrder.forEach((test, index) => { it(`identical aggregations (${index}) - init json is in different order`, () => { - const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry, fieldFormats }); + const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry, fieldFormats }); expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); }); }); @@ -316,8 +318,8 @@ describe('AggConfig', () => { testsDifferent.forEach((test, index) => { it(`different aggregations (${index})`, () => { - const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry, fieldFormats }); + const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry, fieldFormats }); expect(ac1.jsonDataEquals(ac2.aggs)).toBe(false); }); }); @@ -325,7 +327,7 @@ describe('AggConfig', () => { describe('#toJSON', () => { it('includes the aggs id, params, type and schema', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { enabled: true, type: 'date_histogram', @@ -356,8 +358,8 @@ describe('AggConfig', () => { params: {}, }, ]; - const ac1 = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); + const ac2 = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); // this relies on the assumption that js-engines consistently loop over properties in insertion order. // most likely the case, but strictly speaking not guaranteed by the JS and JSON specifications. @@ -369,7 +371,7 @@ describe('AggConfig', () => { let aggConfig: AggConfig; beforeEach(() => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); aggConfig = ac.createAggConfig({ type: 'count' } as CreateAggConfigParams); }); @@ -398,7 +400,7 @@ describe('AggConfig', () => { describe('#fieldFormatter - custom getFormat handler', () => { it('returns formatter from getFormat handler', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { enabled: true, type: 'count', @@ -439,7 +441,7 @@ describe('AggConfig', () => { }, }, }; - const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry, fieldFormats }); aggConfig = ac.createAggConfig(configStates); }); diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index d6948aaade63d..6188849e0e6d4 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -25,7 +25,7 @@ import { IAggConfigs } from './agg_configs'; import { FetchOptions } from '../fetch'; import { ISearchSource } from '../search_source'; import { FieldFormatsContentType, KBN_FIELD_TYPES } from '../../../common'; -import { getFieldFormats } from '../../../public/services'; +import { FieldFormatsStart } from '../../field_formats'; export interface AggConfigOptions { type: IAggType; @@ -35,6 +35,10 @@ export interface AggConfigOptions { schema?: string; } +export interface AggConfigDependencies { + fieldFormats: FieldFormatsStart; +} + /** * @name AggConfig * @@ -93,8 +97,13 @@ export class AggConfig { private __type: IAggType; private __typeDecorations: any; private subAggs: AggConfig[] = []; + private readonly fieldFormats: FieldFormatsStart; - constructor(aggConfigs: IAggConfigs, opts: AggConfigOptions) { + constructor( + aggConfigs: IAggConfigs, + opts: AggConfigOptions, + { fieldFormats }: AggConfigDependencies + ) { this.aggConfigs = aggConfigs; this.id = String(opts.id || AggConfig.nextId(aggConfigs.aggs as any)); this.enabled = typeof opts.enabled === 'boolean' ? opts.enabled : true; @@ -115,6 +124,8 @@ export class AggConfig { // @ts-ignore this.__type = this.__type; + + this.fieldFormats = fieldFormats; } /** @@ -341,12 +352,10 @@ export class AggConfig { } fieldOwnFormatter(contentType?: FieldFormatsContentType, defaultFormat?: any) { - const fieldFormatsService = getFieldFormats(); - const field = this.getField(); let format = field && field.format; if (!format) format = defaultFormat; - if (!format) format = fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.STRING); + if (!format) format = this.fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING); return format.getConverterFor(contentType); } diff --git a/src/plugins/data/public/search/aggs/agg_configs.test.ts b/src/plugins/data/public/search/aggs/agg_configs.test.ts index e20e6de6112a8..653bf6a266df6 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.test.ts @@ -24,10 +24,12 @@ import { AggTypesRegistryStart } from './agg_types_registry'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; import { Field as IndexPatternField, IndexPattern } from '../../index_patterns'; import { stubIndexPattern, stubIndexPatternWithFields } from '../../../public/stubs'; +import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('AggConfigs', () => { let indexPattern: IndexPattern; let typesRegistry: AggTypesRegistryStart; + const fieldFormats = fieldFormatsServiceMock.createStartContract(); beforeEach(() => { mockDataServices(); @@ -45,7 +47,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); expect(ac.aggs).toHaveLength(1); }); @@ -70,7 +72,7 @@ describe('AggConfigs', () => { ]; const spy = jest.spyOn(AggConfig, 'ensureIds'); - new AggConfigs(indexPattern, configStates, { typesRegistry }); + new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); expect(spy).toHaveBeenCalledTimes(1); expect(spy.mock.calls[0]).toEqual([configStates]); spy.mockRestore(); @@ -92,16 +94,20 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); expect(ac.aggs).toHaveLength(2); ac.createAggConfig( - new AggConfig(ac, { - enabled: true, - type: typesRegistry.get('terms'), - params: {}, - schema: 'split', - }) + new AggConfig( + ac, + { + enabled: true, + type: typesRegistry.get('terms'), + params: {}, + schema: 'split', + }, + { fieldFormats } + ) ); expect(ac.aggs).toHaveLength(3); }); @@ -115,7 +121,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); expect(ac.aggs).toHaveLength(1); ac.createAggConfig({ @@ -136,7 +142,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); expect(ac.aggs).toHaveLength(1); ac.createAggConfig( @@ -164,7 +170,7 @@ describe('AggConfigs', () => { { type: 'percentiles', enabled: true, params: {}, schema: 'metric' }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const sorted = ac.getRequestAggs(); const aggs = indexBy(ac.aggs, agg => agg.type.name); @@ -187,7 +193,7 @@ describe('AggConfigs', () => { { type: 'count', enabled: true, params: {}, schema: 'metric' }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const sorted = ac.getResponseAggs(); const aggs = indexBy(ac.aggs, agg => agg.type.name); @@ -204,7 +210,7 @@ describe('AggConfigs', () => { { type: 'percentiles', enabled: true, params: { percents: [1, 2, 3] }, schema: 'metric' }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const sorted = ac.getResponseAggs(); const aggs = indexBy(ac.aggs, agg => agg.type.name); @@ -225,7 +231,7 @@ describe('AggConfigs', () => { it('uses the sorted aggs', () => { const configStates = [{ enabled: true, type: 'avg', params: { field: 'bytes' } }]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const spy = jest.spyOn(AggConfigs.prototype, 'getRequestAggs'); ac.toDsl(); expect(spy).toHaveBeenCalledTimes(1); @@ -241,6 +247,7 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, + fieldFormats, }); const aggInfos = ac.aggs.map(aggConfig => { @@ -284,7 +291,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const dsl = ac.toDsl(); const histo = ac.byName('date_histogram')[0]; const count = ac.byName('count')[0]; @@ -311,6 +318,7 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, + fieldFormats, }); const dsl = ac.toDsl(); const histo = ac.byName('date_histogram')[0]; @@ -336,7 +344,7 @@ describe('AggConfigs', () => { { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const topLevelDsl = ac.toDsl(true); const buckets = ac.bySchemaName('buckets'); const metrics = ac.bySchemaName('metrics'); @@ -406,7 +414,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); const topLevelDsl = ac.toDsl(true)['2']; expect(Object.keys(topLevelDsl.aggs)).toContain('1'); diff --git a/src/plugins/data/public/search/aggs/agg_configs.ts b/src/plugins/data/public/search/aggs/agg_configs.ts index c441b2a0eb46f..5ad09f824d3e4 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.ts @@ -28,6 +28,7 @@ import { IndexPattern } from '../../index_patterns'; import { ISearchSource } from '../search_source'; import { FetchOptions } from '../fetch'; import { TimeRange } from '../../../common'; +import { FieldFormatsStart } from '../../field_formats'; function removeParentAggs(obj: any) { for (const prop in obj) { @@ -47,6 +48,7 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) { export interface AggConfigsOptions { typesRegistry: AggTypesRegistryStart; + fieldFormats: FieldFormatsStart; } export type CreateAggConfigParams = Assign; @@ -68,6 +70,7 @@ export type IAggConfigs = AggConfigs; export class AggConfigs { public indexPattern: IndexPattern; public timeRange?: TimeRange; + private readonly fieldFormats: FieldFormatsStart; private readonly typesRegistry: AggTypesRegistryStart; aggs: IAggConfig[]; @@ -83,6 +86,7 @@ export class AggConfigs { this.aggs = []; this.indexPattern = indexPattern; + this.fieldFormats = opts.fieldFormats; configStates.forEach((params: any) => this.createAggConfig(params)); } @@ -113,6 +117,7 @@ export class AggConfigs { const aggConfigs = new AggConfigs(this.indexPattern, this.aggs.filter(filterAggs), { typesRegistry: this.typesRegistry, + fieldFormats: this.fieldFormats, }); return aggConfigs; @@ -129,10 +134,14 @@ export class AggConfigs { aggConfig = params; params.parent = this; } else { - aggConfig = new AggConfig(this, { - ...params, - type: typeof type === 'string' ? this.typesRegistry.get(type) : type, - }); + aggConfig = new AggConfig( + this, + { + ...params, + type: typeof type === 'string' ? this.typesRegistry.get(type) : type, + }, + { fieldFormats: this.fieldFormats } + ); } if (addToAggConfigs) { diff --git a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts index c664325a168b1..44d99375bbd30 100644 --- a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -26,6 +26,7 @@ import { AggConfigs, CreateAggConfigParams } from '../agg_configs'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketAggConfig } from './bucket_agg_type'; import { mockAggTypesRegistry } from '../test_helpers'; +import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; const indexPattern = { id: '1234', @@ -219,8 +220,10 @@ const nestedOtherResponse = { describe('Terms Agg Other bucket helper', () => { const typesRegistry = mockAggTypesRegistry(); + const fieldFormats = fieldFormatsServiceMock.createStartContract(); + const getAggConfigs = (aggs: CreateAggConfigParams[] = []) => { - return new AggConfigs(indexPattern, [...aggs], { typesRegistry }); + return new AggConfigs(indexPattern, [...aggs], { typesRegistry, fieldFormats }); }; describe('buildOtherBucketAgg', () => { diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index 97c940b4ff4b1..7778fcb36bcd6 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -78,7 +78,10 @@ describe('AggConfig Filters', () => { params: { field: field.name, interval, customInterval: '5d' }, }, ], - { typesRegistry: mockAggTypesRegistry([getDateHistogramBucketAgg(aggTypesDependencies)]) } + { + typesRegistry: mockAggTypesRegistry([getDateHistogramBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); const bucketKey = 1422579600000; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts index 8c0466b769a7e..4207fa92736f8 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts @@ -72,7 +72,10 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry: mockAggTypesRegistry([getDateRangeBucketAgg(aggTypesDependencies)]) } + { + typesRegistry: mockAggTypesRegistry([getDateRangeBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts index f5a0b5a7b9094..bf05f7463db6c 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts @@ -69,7 +69,10 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry: mockAggTypesRegistry([getFiltersBucketAgg(aggTypesDependencies)]) } + { + typesRegistry: mockAggTypesRegistry([getFiltersBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts index 18b388be74877..396d515f3b580 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts @@ -23,10 +23,12 @@ import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common'; +import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; describe('AggConfig Filters', () => { describe('histogram', () => { const getConfig = (() => {}) as FieldFormatsGetConfigFn; + const fieldFormats = fieldFormatsServiceMock.createStartContract(); const getAggConfigs = () => { const field = { name: 'bytes', @@ -55,7 +57,7 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry(), fieldFormats } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts index b528313b080d0..d85576a0ccb14 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts @@ -29,10 +29,11 @@ import { notificationServiceMock } from '../../../../../../../core/public/mocks' describe('AggConfig Filters', () => { describe('IP range', () => { + const fieldFormats = fieldFormatsServiceMock.createStartContract(); const typesRegistry = mockAggTypesRegistry([ getIpRangeBucketAgg({ getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), + fieldFormats, notifications: notificationServiceMock.createStartContract(), }), }), @@ -52,7 +53,7 @@ describe('AggConfig Filters', () => { }, } as any; - return new AggConfigs(indexPattern, aggs, { typesRegistry }); + return new AggConfigs(indexPattern, aggs, { typesRegistry, fieldFormats }); }; test('should return a range filter for ip_range agg', () => { diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts index 14a7538aa95a4..cadd8e9fe13ed 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts @@ -71,7 +71,10 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]) } + { + typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts index c11a7d1a4e6b8..d9ff63613b640 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts @@ -58,6 +58,7 @@ describe('AggConfig Filters', () => { return new AggConfigs(indexPattern, aggs, { typesRegistry: mockAggTypesRegistry([getTermsBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, }); }; diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts index c050620c3a856..f78f0cce732e7 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts @@ -74,7 +74,10 @@ describe('date_range params', () => { params, }, ], - { typesRegistry: mockAggTypesRegistry([getDateRangeBucketAgg(aggTypesDependencies)]) } + { + typesRegistry: mockAggTypesRegistry([getDateRangeBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts index 24270dd33a576..226faefe43482 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -77,7 +77,10 @@ describe('Geohash Agg', () => { }, }, ], - { typesRegistry: mockAggTypesRegistry() } + { + typesRegistry: mockAggTypesRegistry(), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts index bbfc263df4268..a55c32951232a 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts @@ -70,7 +70,10 @@ describe('Histogram Agg', () => { params, }, ], - { typesRegistry: mockAggTypesRegistry([getHistogramBucketAgg(aggTypesDependencies)]) } + { + typesRegistry: mockAggTypesRegistry([getHistogramBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/range.test.ts b/src/plugins/data/public/search/aggs/buckets/range.test.ts index a1f0ab6a2326a..144d2b779e950 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.test.ts @@ -95,7 +95,10 @@ describe('Range Agg', () => { }, }, ], - { typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]) } + { + typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts index 761d0ced6a114..d0ace5a50c28d 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts @@ -70,6 +70,7 @@ describe('Significant Terms Agg', () => { typesRegistry: mockAggTypesRegistry([ getSignificantTermsBucketAgg(aggTypesDependencies), ]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/terms.test.ts b/src/plugins/data/public/search/aggs/buckets/terms.test.ts index 5afe7d0b0c35c..0dc052bd1fdf6 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.test.ts @@ -20,9 +20,11 @@ import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; describe('Terms Agg', () => { describe('order agg editor UI', () => { + const fieldFormats = fieldFormatsServiceMock.createStartContract(); const getAggConfigs = (params: Record = {}) => { const indexPattern = { id: '1234', @@ -47,7 +49,7 @@ describe('Terms Agg', () => { type: BUCKET_TYPES.TERMS, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry(), fieldFormats } ); }; diff --git a/src/plugins/data/public/search/aggs/metrics/median.test.ts b/src/plugins/data/public/search/aggs/metrics/median.test.ts index f80c46026f50a..de3ca646ead9e 100644 --- a/src/plugins/data/public/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/median.test.ts @@ -59,7 +59,10 @@ describe('AggTypeMetricMedianProvider class', () => { }, }, ], - { typesRegistry } + { + typesRegistry, + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } ); }); diff --git a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts index af983a50f6c23..3beb92a2fa000 100644 --- a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts @@ -110,7 +110,7 @@ describe('parent pipeline aggs', function() { schema: 'metric', }, ], - { typesRegistry } + { typesRegistry, fieldFormats: getInternalStartServices().fieldFormats } ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index 2944fc8c11b23..1b94ecd602075 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -70,7 +70,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { }, }, ], - { typesRegistry } + { typesRegistry, fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats } ); }); diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts index 33bd42df74cc7..76da2fe3eb62c 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -70,7 +70,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { }, }, ], - { typesRegistry } + { typesRegistry, fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats } ); }); diff --git a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts index ab480fe44227e..a47aa2c677ade 100644 --- a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts @@ -111,7 +111,7 @@ describe('sibling pipeline aggs', () => { }, }, ], - { typesRegistry } + { typesRegistry, fieldFormats: getInternalStartServices().fieldFormats } ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts index 6bbff3009cc11..d2370e1fed02c 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts @@ -64,7 +64,7 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { }, }, ], - { typesRegistry } + { typesRegistry, fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats } ); }; diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts index 8294ad09bae22..142b8e4c83301 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts @@ -89,7 +89,7 @@ describe('Top hit metric', () => { params, }, ], - { typesRegistry } + { typesRegistry, fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats } ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index 7a5dcc9be4592..16544fd8f46b0 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -27,6 +27,7 @@ import { } from './'; import { SearchAggsSetup, SearchAggsStart } from './types'; import { mockAggTypesRegistry } from './test_helpers'; +import { fieldFormatsServiceMock } from '../../field_formats/mocks'; const aggTypeBaseParamMock = () => ({ name: 'some_param', @@ -72,6 +73,7 @@ export const searchAggsStartMock = (): SearchAggsStart => ({ createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), }); }), types: mockAggTypesRegistry(), diff --git a/src/plugins/data/public/search/expressions/create_filter.test.ts b/src/plugins/data/public/search/expressions/create_filter.test.ts index 23da060cba203..51b5e175761bd 100644 --- a/src/plugins/data/public/search/expressions/create_filter.test.ts +++ b/src/plugins/data/public/search/expressions/create_filter.test.ts @@ -22,10 +22,12 @@ import { AggConfigs, IAggConfig } from '../aggs'; import { TabbedTable } from '../tabify'; import { isRangeFilter, BytesFormat, FieldFormatsGetConfigFn } from '../../../common'; import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; +import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('createFilter', () => { let table: TabbedTable; let aggConfig: IAggConfig; + const fieldFormats = fieldFormatsServiceMock.createStartContract(); const typesRegistry = mockAggTypesRegistry(); @@ -58,7 +60,7 @@ describe('createFilter', () => { params, }, ], - { typesRegistry } + { typesRegistry, fieldFormats } ); }; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6124682184821..a539736991adb 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -44,12 +44,19 @@ import { siblingPipelineAggHelper, } from './aggs'; +import { FieldFormatsStart } from '../field_formats'; + interface SearchServiceSetupDependencies { packageInfo: PackageInfo; query: QuerySetup; getInternalStartServices: GetInternalStartServicesFn; } +interface SearchStartDependencies { + fieldFormats: FieldFormatsStart; + indexPatterns: IndexPatternsContract; +} + /** * The search plugin exposes two registration methods for other plugins: * - registerSearchStrategyProvider for plugins to add their own custom @@ -110,7 +117,10 @@ export class SearchService implements Plugin { }; } - public start(core: CoreStart, indexPatterns: IndexPatternsContract): ISearchStart { + public start( + core: CoreStart, + { fieldFormats, indexPatterns }: SearchStartDependencies + ): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -131,6 +141,7 @@ export class SearchService implements Plugin { createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: aggTypesStart, + fieldFormats, }); }, types: aggTypesStart, diff --git a/src/plugins/data/public/search/tabify/get_columns.test.ts b/src/plugins/data/public/search/tabify/get_columns.test.ts index b7dadc3f65d82..1072e9318b40e 100644 --- a/src/plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/plugins/data/public/search/tabify/get_columns.test.ts @@ -21,6 +21,7 @@ import { tabifyGetColumns } from './get_columns'; import { TabbedAggColumn } from './types'; import { AggConfigs } from '../aggs'; import { mockAggTypesRegistry, mockDataServices } from '../aggs/test_helpers'; +import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('get columns', () => { beforeEach(() => { @@ -28,6 +29,7 @@ describe('get columns', () => { }); const typesRegistry = mockAggTypesRegistry(); + const fieldFormats = fieldFormatsServiceMock.createStartContract(); const createAggConfigs = (aggs: any[] = []) => { const field = { @@ -45,6 +47,7 @@ describe('get columns', () => { return new AggConfigs(indexPattern, aggs, { typesRegistry, + fieldFormats, }); }; diff --git a/src/plugins/data/public/search/tabify/response_writer.test.ts b/src/plugins/data/public/search/tabify/response_writer.test.ts index 52338ae79ccbb..3334d858ce54e 100644 --- a/src/plugins/data/public/search/tabify/response_writer.test.ts +++ b/src/plugins/data/public/search/tabify/response_writer.test.ts @@ -21,6 +21,7 @@ import { TabbedAggResponseWriter } from './response_writer'; import { AggConfigs, BUCKET_TYPES } from '../aggs'; import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; import { TabbedResponseWriterOptions } from './types'; +import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('TabbedAggResponseWriter class', () => { beforeEach(() => { @@ -30,6 +31,7 @@ describe('TabbedAggResponseWriter class', () => { let responseWriter: TabbedAggResponseWriter; const typesRegistry = mockAggTypesRegistry(); + const fieldFormats = fieldFormatsServiceMock.createStartContract(); const splitAggConfig = [ { @@ -74,6 +76,7 @@ describe('TabbedAggResponseWriter class', () => { return new TabbedAggResponseWriter( new AggConfigs(indexPattern, aggs, { typesRegistry, + fieldFormats, }), { metricsAtAllLevels: false, diff --git a/src/plugins/data/public/search/tabify/tabify.test.ts b/src/plugins/data/public/search/tabify/tabify.test.ts index c9bf04ae9f0fc..63685cc87f5cf 100644 --- a/src/plugins/data/public/search/tabify/tabify.test.ts +++ b/src/plugins/data/public/search/tabify/tabify.test.ts @@ -22,9 +22,11 @@ import { IndexPattern } from '../../index_patterns'; import { AggConfigs, IAggConfig, IAggConfigs } from '../aggs'; import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; +import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('tabifyAggResponse Integration', () => { const typesRegistry = mockAggTypesRegistry(); + const fieldFormats = fieldFormatsServiceMock.createStartContract(); const createAggConfigs = (aggs: IAggConfig[] = []) => { const field = { @@ -42,6 +44,7 @@ describe('tabifyAggResponse Integration', () => { return new AggConfigs(indexPattern, aggs, { typesRegistry, + fieldFormats, }); }; diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index 63f6997ce2fc3..ded48d462722d 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -61,6 +61,7 @@ export function QueryLanguageSwitcher(props: Props) { size="xs" onClick={() => setIsPopoverOpen(!isPopoverOpen)} className="euiFormControlLayout__append" + data-test-subj={'switchQueryLanguageButton'} > {props.language === 'lucene' ? luceneLabel : kqlLabel} diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 5ca334d6bdcfe..7723254f3aa51 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -124,7 +124,8 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) const onQuerySubmitRef = useRef(props.onQuerySubmit); const defaultQuery = { query: '', - language: core.uiSettings.get('search:queryLanguage'), + language: + storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'), }; const [query, setQuery] = useState(props.query || defaultQuery); @@ -161,7 +162,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) setQuery, savedQueryId: props.savedQueryId, notifications: core.notifications, - uiSettings: core.uiSettings, + defaultLanguage: defaultQuery.language, }); // Fire onQuerySubmit on query or timerange change diff --git a/src/plugins/data/public/ui/search_bar/lib/use_saved_query.ts b/src/plugins/data/public/ui/search_bar/lib/use_saved_query.ts index 817e890b7b42b..79aee3438d7aa 100644 --- a/src/plugins/data/public/ui/search_bar/lib/use_saved_query.ts +++ b/src/plugins/data/public/ui/search_bar/lib/use_saved_query.ts @@ -29,8 +29,8 @@ interface UseSavedQueriesProps { queryService: DataPublicPluginStart['query']; setQuery: Function; notifications: CoreStart['notifications']; - uiSettings: CoreStart['uiSettings']; savedQueryId?: string; + defaultLanguage: string; } interface UseSavedQueriesReturn { @@ -41,7 +41,7 @@ interface UseSavedQueriesReturn { export const useSavedQuery = (props: UseSavedQueriesProps): UseSavedQueriesReturn => { // Handle saved queries - const defaultLanguage = props.uiSettings.get('search:queryLanguage'); + const defaultLanguage = props.defaultLanguage; const [savedQuery, setSavedQuery] = useState(); // Effect is used to convert a saved query id into an object diff --git a/src/plugins/embeddable/README.md b/src/plugins/embeddable/README.md index 1fe91f426f43f..343a1cc32cd10 100644 --- a/src/plugins/embeddable/README.md +++ b/src/plugins/embeddable/README.md @@ -1,39 +1,25 @@ -# The Embeddable API V2 +# Embeddables -The Embeddable API's main goal is to have documented and standardized ways to share and exchange information and functionality across applications and plugins. +Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable _containers_. -There are three main pieces of this infrastructure: - - Embeddables & Containers - - Actions - - Triggers +## Embeddable containers -## Embeddables & Containers +Containers are a special type of embeddable that can contain nested embeddables. Embeddables can be dynamically added to embeddable _containers_. Currently only dashboard uses this interface. -Embeddables are isolated, serializable, renderable widgets. A developer can hard code an embeddable inside their -application, or they can use some built in actions to allow users to dynamically add them to *containers*. - -Containers are a special type of embeddable that can contain nested embeddables. - -## Actions - -Actions are pluggable pieces of functionality exposed to the user that take an embeddable as context, plus an optional action context. - -## Triggers - -Triggers are the way actions are connected to a user action. We ship with two default triggers, `CONTEXT_MENU_TRIGGER` and `APPLY_FILTER`. - -Actions attached to the `CONTEXT_MENU_TRIGGER` will be displayed in supported embeddables context menu to the user. Actions attached to the `APPLY_FILTER` trigger will show up when any embeddable emits this trigger. +## Examples -A developer can register new triggers that their embeddables, or external components, can emit (as long as they have an embeddable to pass along as context). +Many example embeddables are implemented and registered [here](https://github.com/elastic/kibana/tree/master/examples/embeddable_examples). They can be played around with and explored [in the Embeddable Explorer example plugin](https://github.com/elastic/kibana/tree/master/examples/embeddable_explorer). Just run kibana with -## Examples +``` +yarn start --run-examples +``` -Many examples can be viewed in the functionally tested `kbn_tp_embeddable_explorer` plugin, as well as the jest tested classes inside the `embeddable_api/public/test_samples` folder. +and navigate to the Embeddable explorer app. ## Testing Run unit tests ```shell -node scripts/jest embeddable_api +node scripts/jest embeddable ``` diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 23275fbe8e8f0..bdb7bfbddc308 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -61,6 +61,8 @@ export { PropertySpec, ViewMode, withEmbeddableSubscription, + SavedObjectEmbeddableInput, + isSavedObjectEmbeddableInput, } from './lib'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index ce733bba6dda5..d07bf915845e9 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -56,7 +56,7 @@ test('getHref returns the edit urls', async () => { if (action.getHref) { const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true); expect( - action.getHref({ + await action.getHref({ embeddable, }) ).toBe(embeddable.getOutput().editUrl); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 9125dc0813f98..044e7b5d35ad8 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -62,11 +62,16 @@ export class EditPanelAction implements Action { return Boolean(canEditEmbeddable && inDashboardEditMode); } - public async execute() { - return; + public async execute(context: ActionContext) { + const href = await this.getHref(context); + if (href) { + // TODO: when apps start using browser router instead of hash router this has to be fixed + // https://github.com/elastic/kibana/issues/58217 + window.location.href = href; + } } - public getHref({ embeddable }: ActionContext): string { + public async getHref({ embeddable }: ActionContext): Promise { const editUrl = embeddable ? embeddable.getOutput().editUrl : undefined; return editUrl ? editUrl : ''; } diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 4ab74e1883917..ffbe75f66b581 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -30,6 +30,7 @@ import { import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; +import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -98,17 +99,6 @@ export abstract class Container< return this.createAndSaveEmbeddable(type, panelState); } - public async addSavedObjectEmbeddable< - TEmbeddableInput extends EmbeddableInput = EmbeddableInput, - TEmbeddable extends IEmbeddable = IEmbeddable - >(type: string, savedObjectId: string): Promise { - const factory = this.getFactory(type) as EmbeddableFactory; - const panelState = this.createNewPanelState(factory); - panelState.savedObjectId = savedObjectId; - - return this.createAndSaveEmbeddable(type, panelState); - } - public removeEmbeddable(embeddableId: string) { // Just a shortcut for removing the panel from input state, all internal state will get cleaned up naturally // by the listener. @@ -304,8 +294,10 @@ export abstract class Container< throw new EmbeddableFactoryNotFoundError(panel.type); } - embeddable = panel.savedObjectId - ? await factory.createFromSavedObject(panel.savedObjectId, inputForChild, this) + // TODO: lets get rid of this distinction with factories, I don't think it will be needed + // anymore after this change. + embeddable = isSavedObjectEmbeddableInput(inputForChild) + ? await factory.createFromSavedObject(inputForChild.savedObjectId, inputForChild, this) : await factory.create(inputForChild, this); } catch (e) { embeddable = new ErrorEmbeddable(e, { id: panel.explicitInput.id }, this); @@ -323,23 +315,6 @@ export abstract class Container< return; } - if (embeddable.getOutput().savedObjectId) { - this.updateInput({ - panels: { - ...this.input.panels, - [panel.explicitInput.id]: { - ...this.input.panels[panel.explicitInput.id], - ...(embeddable.getOutput().savedObjectId - ? { savedObjectId: embeddable.getOutput().savedObjectId } - : undefined), - explicitInput: { - ...this.input.panels[panel.explicitInput.id].explicitInput, - }, - }, - }, - } as Partial); - } - this.children[embeddable.id] = embeddable; this.updateOutput({ embeddableLoaded: { diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index 7da5f92ec92c1..31a7cd4f2e559 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -25,9 +25,7 @@ import { IEmbeddable, } from '../embeddables'; -export interface PanelState { - savedObjectId?: string; - +export interface PanelState { // The type of embeddable in this panel. Will be used to find the factory in which to // load the embeddable. type: string; @@ -89,17 +87,6 @@ export interface IContainer< */ removeEmbeddable(embeddableId: string): void; - /** - * Adds a new embeddable that is backed off of a saved object. - */ - addSavedObjectEmbeddable< - EEI extends EmbeddableInput = EmbeddableInput, - E extends Embeddable = Embeddable - >( - type: string, - savedObjectId: string - ): Promise; - /** * Adds a new embeddable to the container. `explicitInput` may partially specify the required embeddable input, * but the remainder must come from inherited container state. diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 6345c34b0dda2..c0fb98d2559db 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -26,6 +26,11 @@ import { TriggerContextMapping } from '../../../../ui_actions/public'; export interface EmbeddableInput { viewMode?: ViewMode; title?: string; + /** + * Note this is not a saved object id. It is used to uniquely identify this + * Embeddable instance from others (e.g. inside a container). It's possible to + * have two Embeddables where everything else is the same but the id. + */ id: string; lastReloadRequestTime?: number; hidePanelTitles?: boolean; @@ -44,6 +49,8 @@ export interface EmbeddableInput { * Whether this embeddable should not execute triggers. */ disableTriggers?: boolean; + + [key: string]: unknown; } export interface EmbeddableOutput { diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 4d6ab37a50c05..ad8b9c35e60be 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -25,3 +25,4 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableFactoryRenderer } from './embeddable_factory_renderer'; export { EmbeddableRoot } from './embeddable_root'; +export * from './saved_object_embeddable'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts new file mode 100644 index 0000000000000..6ca1800b16de4 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.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 { EmbeddableInput } from '..'; + +export interface SavedObjectEmbeddableInput extends EmbeddableInput { + savedObjectId: string; +} + +export function isSavedObjectEmbeddableInput( + input: EmbeddableInput | SavedObjectEmbeddableInput +): input is SavedObjectEmbeddableInput { + return (input as SavedObjectEmbeddableInput).savedObjectId !== undefined; +} 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 50d8bcef8506c..5bf3f69a95c30 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 @@ -33,6 +33,7 @@ import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { IContainer } from '../../../../containers'; import { EmbeddableFactoryNotFoundError } from '../../../../errors'; import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new'; +import { SavedObjectEmbeddableInput } from '../../../../embeddables'; interface Props { onClose: () => void; @@ -98,8 +99,18 @@ export class AddPanelFlyout extends React.Component { } }; - public onAddPanel = async (id: string, type: string, name: string) => { - this.props.container.addSavedObjectEmbeddable(type, id); + public onAddPanel = async (savedObjectId: string, savedObjectType: string, name: string) => { + const factoryForSavedObjectType = [...this.props.getAllFactories()].find( + factory => factory.savedObjectMetaData && factory.savedObjectMetaData.type === savedObjectType + ); + if (!factoryForSavedObjectType) { + throw new EmbeddableFactoryNotFoundError(savedObjectType); + } + + this.props.container.addNewEmbeddable( + factoryForSavedObjectType.type, + { savedObjectId } + ); this.showToast(name); }; diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index 87076399465d3..1aae43550ec6f 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -641,8 +641,7 @@ test('container stores ErrorEmbeddables when a saved object cannot be found', as panels: { '123': { type: 'vis', - explicitInput: { id: '123' }, - savedObjectId: '456', + explicitInput: { id: '123', savedObjectId: '456' }, }, }, viewMode: ViewMode.EDIT, @@ -663,8 +662,7 @@ test('ErrorEmbeddables get updated when parent does', async done => { panels: { '123': { type: 'vis', - explicitInput: { id: '123' }, - savedObjectId: '456', + explicitInput: { id: '123', savedObjectId: '456' }, }, }, viewMode: ViewMode.EDIT, diff --git a/src/plugins/expressions/server/legacy.ts b/src/plugins/expressions/server/legacy.ts index 17aa1c66a6835..1487f9f6734e9 100644 --- a/src/plugins/expressions/server/legacy.ts +++ b/src/plugins/expressions/server/legacy.ts @@ -26,7 +26,7 @@ import { register, registryFactory, Registry, Fn } from '@kbn/interpreter/common import Boom from 'boom'; import { schema } from '@kbn/config-schema'; -import { CoreSetup, Logger } from 'src/core/server'; +import { CoreSetup, Logger, APICaller } from 'src/core/server'; import { ExpressionsServerSetupDependencies } from './plugin'; import { typeSpecs, ExpressionType } from '../common'; import { serializeProvider } from '../common'; @@ -97,7 +97,10 @@ export const createLegacyServerEndpoints = ( * @param {*} handlers - The Canvas handlers * @param {*} fnCall - Describes the function being run `{ functionName, args, context }` */ - async function runFunction(handlers: any, fnCall: any) { + async function runFunction( + handlers: { environment: string; elasticsearchClient: APICaller }, + fnCall: any + ) { const { functionName, args, context } = fnCall; const { deserialize } = serializeProvider(registries.types.toJS()); const fnDef = registries.serverFunctions.toJS()[functionName]; @@ -112,18 +115,14 @@ export const createLegacyServerEndpoints = ( * results back using ND-JSON. */ plugins.bfetch.addBatchProcessingRoute(`/api/interpreter/fns`, request => { - const scopedClient = core.elasticsearch.dataClient.asScoped(request); - const handlers = { - environment: 'server', - elasticsearchClient: async ( - endpoint: string, - clientParams: Record = {}, - options?: any - ) => scopedClient.callAsCurrentUser(endpoint, clientParams, options), - }; - return { onBatchItem: async (fnCall: any) => { + const [coreStart] = await core.getStartServices(); + const handlers = { + environment: 'server', + elasticsearchClient: coreStart.elasticsearch.legacy.client.asScoped(request) + .callAsCurrentUser, + }; const result = await runFunction(handlers, fnCall); if (typeof result === 'undefined') { throw new Error(`Function ${fnCall.functionName} did not return anything.`); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index f532c2c8aa219..feaa1f6a60e2f 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -63,9 +63,11 @@ export interface Action { isCompatible(context: Context): Promise; /** - * If this returns something truthy, this is used in addition to the `execute` method when clicked. + * If this returns something truthy, this will be used as [href] attribute on a link if possible (e.g. in context menu item) + * to support right click -> open in a new tab behavior. + * For regular click navigation is prevented and `execute()` takes control. */ - getHref?(context: Context): string | undefined; + getHref?(context: Context): Promise; /** * Executes the action. diff --git a/src/plugins/ui_actions/public/actions/action_definition.ts b/src/plugins/ui_actions/public/actions/action_definition.ts index 3eaa13572a826..79fda78401abd 100644 --- a/src/plugins/ui_actions/public/actions/action_definition.ts +++ b/src/plugins/ui_actions/public/actions/action_definition.ts @@ -63,7 +63,7 @@ export interface ActionDefinition { /** * If this returns something truthy, this is used in addition to the `execute` method when clicked. */ - getHref?(context: ActionContextMapping[T]): string | undefined; + getHref?(context: ActionContextMapping[T]): Promise; /** * Executes the action. diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index 90a9415c0b497..cc66f221e4082 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -28,7 +28,6 @@ export function createAction(action: ActionDefinition): id: action.type, isCompatible: () => Promise.resolve(true), getDisplayName: () => '', - getHref: () => undefined, ...action, }; } diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 3dce2c1f4c257..d26740ffdf033 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -71,7 +71,7 @@ async function buildEuiContextMenuPanelItems({ } items.push( - convertPanelActionToContextMenuItem({ + await convertPanelActionToContextMenuItem({ action, actionContext, closeMenu, @@ -88,9 +88,9 @@ async function buildEuiContextMenuPanelItems({ * * @param {ContextMenuAction} action * @param {Embeddable} embeddable - * @return {EuiContextMenuPanelItemDescriptor} + * @return {Promise} */ -function convertPanelActionToContextMenuItem({ +async function convertPanelActionToContextMenuItem({ action, actionContext, closeMenu, @@ -98,7 +98,7 @@ function convertPanelActionToContextMenuItem({ action: Action; actionContext: A; closeMenu: () => void; -}): EuiContextMenuPanelItemDescriptor { +}): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { name: action.MenuItem ? React.createElement(uiToReactComponent(action.MenuItem), { @@ -110,13 +110,33 @@ function convertPanelActionToContextMenuItem({ 'data-test-subj': `embeddablePanelAction-${action.id}`, }; - menuPanelItem.onClick = () => { - action.execute(actionContext); + menuPanelItem.onClick = event => { + if (event.currentTarget instanceof HTMLAnchorElement) { + // from react-router's + if ( + !event.defaultPrevented && // onClick prevented default + event.button === 0 && // ignore everything but left clicks + (!event.currentTarget.target || event.currentTarget.target === '_self') && // let browser handle "target=_blank" etc. + !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys + ) { + event.preventDefault(); + action.execute(actionContext); + } else { + // let browser handle navigation + } + } else { + // not a link + action.execute(actionContext); + } + closeMenu(); }; - if (action.getHref && action.getHref(actionContext)) { - menuPanelItem.href = action.getHref(actionContext); + if (action.getHref) { + const href = await action.getHref(actionContext); + if (href) { + menuPanelItem.href = href; + } } return menuPanelItem; diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index 5b670df354f78..1fc92d7c0cb1b 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -55,13 +55,6 @@ export class TriggerInternal { action: Action, context: TriggerContextMapping[T] ) { - const href = action.getHref && action.getHref(context); - - if (href) { - window.location.href = href; - return; - } - await action.execute(context); } diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json index d77f4ac92da16..305e159ac2505 100644 --- a/src/plugins/vis_type_timeseries/kibana.json +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -3,5 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "server": true, + "ui": true, + "requiredPlugins": ["data", "expressions", "visualizations"], "optionalPlugins": ["usageCollection"] } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/_mixins.scss b/src/plugins/vis_type_timeseries/public/application/_mixins.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/_mixins.scss rename to src/plugins/vis_type_timeseries/public/application/_mixins.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/_tvb_editor.scss b/src/plugins/vis_type_timeseries/public/application/_tvb_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/_tvb_editor.scss rename to src/plugins/vis_type_timeseries/public/application/_tvb_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/_variables.scss b/src/plugins/vis_type_timeseries/public/application/_variables.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/_variables.scss rename to src/plugins/vis_type_timeseries/public/application/_variables.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_annotations_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_annotations_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_annotations_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_annotations_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_color_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_color_picker.scss rename to src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_color_rules.scss b/src/plugins/vis_type_timeseries/public/application/components/_color_rules.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_color_rules.scss rename to src/plugins/vis_type_timeseries/public/application/components/_color_rules.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_custom_color_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_custom_color_picker.scss rename to src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_error.scss b/src/plugins/vis_type_timeseries/public/application/components/_error.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_error.scss rename to src/plugins/vis_type_timeseries/public/application/components/_error.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_markdown_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_markdown_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_markdown_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_markdown_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_no_data.scss b/src/plugins/vis_type_timeseries/public/application/components/_no_data.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_no_data.scss rename to src/plugins/vis_type_timeseries/public/application/components/_no_data.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_series_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_series_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_series_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_series_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor_visualization.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_editor_visualization.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor_visualization.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_editor_visualization.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_picker.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_picker.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_picker.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_with_splits.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_with_splits.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_with_splits.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_with_splits.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.js rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.test.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.test.js rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_agg_row.scss b/src/plugins/vis_type_timeseries/public/application/components/aggs/_agg_row.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_agg_row.scss rename to src/plugins/vis_type_timeseries/public/application/components/aggs/_agg_row.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/aggs/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/aggs/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_row.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/aggs.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/aggs.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/cumulative_sum.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/derivative.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/field_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/field_select.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/filter_ratio.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/math.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/metric_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/metric_select.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/moving_average.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/index.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/index.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/index.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/multi_value_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/multi_value_row.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank_values.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank_values.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_ui.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_ui.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_only.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/serial_diff.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/series_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/series_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/series_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/series_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/static.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/static.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/static.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/static.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_deviation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_deviation.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_sibling.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/temporary_unsupported_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/temporary_unsupported_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/top_hit.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/unsupported_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/unsupported_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/unsupported_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/unsupported_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/vars.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/annotations_editor.js b/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/annotations_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.js b/src/plugins/vis_type_timeseries/public/application/components/color_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/color_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.test.js b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.test.js rename to src/plugins/vis_type_timeseries/public/application/components/color_picker.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.js rename to src/plugins/vis_type_timeseries/public/application/components/color_rules.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.test.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.test.js rename to src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js b/src/plugins/vis_type_timeseries/public/application/components/custom_color_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/custom_color_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/data_format_picker.js b/src/plugins/vis_type_timeseries/public/application/components/data_format_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/data_format_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/data_format_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/error.js b/src/plugins/vis_type_timeseries/public/application/components/error.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/error.js rename to src/plugins/vis_type_timeseries/public/application/components/error.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.js b/src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.js rename to src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.test.js b/src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.test.js rename to src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/index_pattern.js rename to src/plugins/vis_type_timeseries/public/application/components/index_pattern.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js b/src/plugins/vis_type_timeseries/public/application/components/lib/agg_to_component.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/agg_to_component.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.js b/src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/charts.js b/src/plugins/vis_type_timeseries/public/application/components/lib/charts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/charts.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/charts.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.js b/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.js similarity index 82% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.js index 79cbe98b3d3db..3eae18111bb4d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.js @@ -18,7 +18,6 @@ */ import uuid from 'uuid'; -import _ from 'lodash'; const newFn = () => ({ id: uuid.v1() }); @@ -30,9 +29,7 @@ export function handleChange(props, doc) { if (row.id === doc.id) return doc; return row; }); - if (_.isFunction(props.onChange)) { - props.onChange(_.assign({}, model, part)); - } + props.onChange?.({ ...model, ...part }); } export function handleDelete(props, doc) { @@ -40,20 +37,15 @@ export function handleDelete(props, doc) { const collection = model[name] || []; const part = {}; part[name] = collection.filter(row => row.id !== doc.id); - if (_.isFunction(props.onChange)) { - props.onChange(_.assign({}, model, part)); - } + props.onChange?.({ ...model, ...part }); } export function handleAdd(props, fn = newFn) { - if (!_.isFunction(fn)) fn = newFn; const { model, name } = props; const collection = model[name] || []; const part = {}; part[name] = collection.concat([fn()]); - if (_.isFunction(props.onChange)) { - props.onChange(_.assign({}, model, part)); - } + props.onChange?.({ ...model, ...part }); } export const collectionActions = { handleAdd, handleDelete, handleChange }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_change_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_change_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_change_handler.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.js similarity index 92% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.js index fd432add43295..3bc8fd87f73dd 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.js @@ -25,8 +25,6 @@ export const createNumberHandler = handleChange => { if (!detectIE() || e.keyCode === 13) e.preventDefault(); const value = Number(_.get(e, 'target.value', defaultValue)); - if (_.isFunction(handleChange)) { - return handleChange({ [name]: value }); - } + return handleChange?.({ [name]: value }); }; }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js similarity index 86% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js index 30937b97d353f..cfc6a4dc57871 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js @@ -21,10 +21,8 @@ import _ from 'lodash'; export const createSelectHandler = handleChange => { return name => selectedOptions => { - if (_.isFunction(handleChange)) { - return handleChange({ - [name]: _.get(selectedOptions, '[0].value', null), - }); - } + return handleChange?.({ + [name]: _.get(selectedOptions, '[0].value', null), + }); }; }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.js similarity index 92% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.js index 8e2624488d954..82cc071b59d51 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.js @@ -26,8 +26,6 @@ export const createTextHandler = handleChange => { if (!detectIE() || e.keyCode === 13) e.preventDefault(); const value = _.get(e, 'target.value', defaultValue); - if (_.isFunction(handleChange)) { - return handleChange({ [name]: value }); - } + return handleChange?.({ [name]: value }); }; }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_xaxis_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_xaxis_formatter.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/detect_ie.js b/src/plugins/vis_type_timeseries/public/application/components/lib/detect_ie.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/detect_ie.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/detect_ie.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.js b/src/plugins/vis_type_timeseries/public/application/components/lib/durations.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/durations.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/durations.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/durations.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js similarity index 94% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js index 26723da5ab5c9..972f937ad109d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js @@ -17,7 +17,7 @@ * under the License. */ -import { getUISettings } from '../../services'; +import { getUISettings } from '../../../services'; export function getDefaultQueryLanguage() { return getUISettings().get('search:queryLanguage'); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_display_name.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_display_name.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_display_name.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_display_name.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_interval.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_interval.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_metric_agg_fn.js b/src/plugins/vis_type_timeseries/public/application/components/lib/new_metric_agg_fn.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_metric_agg_fn.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/new_metric_agg_fn.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_series_fn.js b/src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_series_fn.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.js b/src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/reorder.js b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/reorder.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/series_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/series_change_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/stacked.js b/src/plugins/vis_type_timeseries/public/application/components/lib/stacked.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/stacked.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/stacked.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js similarity index 97% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index 3ab8e0f6b885e..fd316e66a16fb 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -20,7 +20,7 @@ import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; -import { getFieldFormats } from '../../services'; +import { getFieldFormats } from '../../../services'; export const createTickFormatter = (format = '0,0.[00]', template, getConfig = null) => { const fieldFormats = getFieldFormats(); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js similarity index 98% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js index e87cba126bb46..ee10b254a9e15 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js @@ -19,7 +19,7 @@ import { createTickFormatter } from './tick_formatter'; import { getFieldFormatsRegistry } from '../../../../../../test_utils/public/stub_field_formats'; -import { setFieldFormats } from '../../services'; +import { setFieldFormats } from '../../../services'; const mockUiSettings = { get: item => { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/markdown_editor.js b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/markdown_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/no_data.js b/src/plugins/vis_type_timeseries/public/application/components/no_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/no_data.js rename to src/plugins/vis_type_timeseries/public/application/components/no_data.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/panel_config/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_panel_config.scss b/src/plugins/vis_type_timeseries/public/application/components/panel_config/_panel_config.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_panel_config.scss rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/_panel_config.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/markdown.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/timeseries.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/timeseries.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js b/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js rename to src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series.js b/src/plugins/vis_type_timeseries/public/application/components/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series.js rename to src/plugins/vis_type_timeseries/public/application/components/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series_config.js rename to src/plugins/vis_type_timeseries/public/application/components/series_config.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series_drag_handler.js b/src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series_drag_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series_editor.js b/src/plugins/vis_type_timeseries/public/application/components/series_editor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/series_editor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/split.js b/src/plugins/vis_type_timeseries/public/application/components/split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/split.js rename to src/plugins/vis_type_timeseries/public/application/components/split.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/everything.js b/src/plugins/vis_type_timeseries/public/application/components/splits/everything.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/everything.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/everything.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter.js b/src/plugins/vis_type_timeseries/public/application/components/splits/filter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/filter.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter_items.js b/src/plugins/vis_type_timeseries/public/application/components/splits/filter_items.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter_items.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/filter_items.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filters.js b/src/plugins/vis_type_timeseries/public/application/components/splits/filters.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filters.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/filters.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/group_by_select.js b/src/plugins/vis_type_timeseries/public/application/components/splits/group_by_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/group_by_select.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/group_by_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/terms.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.test.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.test.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/terms.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/unsupported_split.js b/src/plugins/vis_type_timeseries/public/application/components/splits/unsupported_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/unsupported_split.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/unsupported_split.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/svg/bomb_icon.js b/src/plugins/vis_type_timeseries/public/application/components/svg/bomb_icon.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/svg/bomb_icon.js rename to src/plugins/vis_type_timeseries/public/application/components/svg/bomb_icon.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/svg/fire_icon.js b/src/plugins/vis_type_timeseries/public/application/components/svg/fire_icon.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/svg/fire_icon.js rename to src/plugins/vis_type_timeseries/public/application/components/svg/fire_icon.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js similarity index 99% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index b4845696fc8c0..7075e86eb56bf 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -30,7 +30,7 @@ import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; import { esKuery } from '../../../../../plugins/data/public'; -import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../services'; +import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; @@ -96,7 +96,7 @@ export class VisEditor extends Component { return true; }; - handleChange = async partialModel => { + handleChange = partialModel => { if (isEmpty(partialModel)) { return; } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor_visualization.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_picker.js b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/_markdown.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/_markdown.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/_markdown.scss rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/_markdown.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/is_sortable.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/is_sortable.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js similarity index 99% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index 1fe9358cbfea9..c6f1db149928c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -27,7 +27,7 @@ import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; import { fieldFormats } from '../../../../../../../plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getFieldFormats } from '../../../services'; +import { getFieldFormats } from '../../../../services'; import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/config.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js similarity index 99% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index f559bc38b6c58..1004f7ee96a54 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -34,7 +34,7 @@ import { getInterval } from '../../lib/get_interval'; import { areFieldsDifferent } from '../../lib/charts'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; -import { getCoreStart, getUISettings } from '../../../services'; +import { getCoreStart, getUISettings } from '../../../../services'; export class TimeseriesVisualization extends Component { static propTypes = { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_with_splits.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/visualization.js b/src/plugins/vis_type_timeseries/public/application/components/visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/visualization.js rename to src/plugins/vis_type_timeseries/public/application/components/visualization.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.js b/src/plugins/vis_type_timeseries/public/application/components/yes_no.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.js rename to src/plugins/vis_type_timeseries/public/application/components/yes_no.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.test.js b/src/plugins/vis_type_timeseries/public/application/components/yes_no.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.test.js rename to src/plugins/vis_type_timeseries/public/application/components/yes_no.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/form_validation_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/contexts/form_validation_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts b/src/plugins/vis_type_timeseries/public/application/contexts/query_input_bar_context.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts rename to src/plugins/vis_type_timeseries/public/application/contexts/query_input_bar_context.ts diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/vis_data_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/contexts/vis_data_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js similarity index 94% rename from src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js rename to src/plugins/vis_type_timeseries/public/application/editor_controller.js index 16a6348712065..af50d3a06d1fc 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -20,7 +20,8 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { fetchIndexPatternFields } from './lib/fetch_fields'; -import { getSavedObjectsClient, getUISettings, getI18n } from './services'; +import { getSavedObjectsClient, getUISettings, getI18n } from '../services'; +import { VisEditor } from './components/vis_editor'; export class EditorController { constructor(el, vis, eventEmitter, embeddableHandler) { @@ -55,19 +56,14 @@ export class EditorController { this.state.isLoaded = true; }; - getComponent = () => { - return this.state.vis.type.editorConfig.component; - }; - async render(params) { - const Component = this.getComponent(); const I18nContext = getI18n().Context; !this.state.isLoaded && (await this.fetchDefaultParams()); render( - getUISettings().get('theme:darkMode'); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js rename to src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/chart.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/chart.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/icons.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/icons.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/index.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/active_cursor.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/active_cursor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/active_cursor.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/active_cursor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calc_dimensions.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/calc_dimensions.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calc_dimensions.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/calc_dimensions.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calculate_coordinates.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/calculate_coordinates.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calculate_coordinates.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/calculate_coordinates.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/get_value_by.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/get_value_by.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/get_value_by.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/get_value_by.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_annotation.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_annotation.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_annotation.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_annotation.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_gauge.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_gauge.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_index.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_index.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_metric.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_metric.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_metric.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_metric.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_top_n.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_top_n.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_top_n.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_top_n.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/annotation.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/annotation.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/annotation.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/annotation.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/__mocks__/@elastic/charts.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/__mocks__/@elastic/charts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/__mocks__/@elastic/charts.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/__mocks__/@elastic/charts.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js similarity index 99% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 3ce3aae2649e1..b1c3c7ac6b67a 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -33,7 +33,7 @@ import { import { EuiIcon } from '@elastic/eui'; import { getTimezone } from '../../../lib/get_timezone'; import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor'; -import { getUISettings } from '../../../services'; +import { getUISettings } from '../../../../services'; import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants'; import { AreaSeriesDecorator } from './decorators/area_decorator'; import { BarSeriesDecorator } from './decorators/bar_decorator'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.test.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.test.ts rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/top_n.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/index.ts b/src/plugins/vis_type_timeseries/public/index.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_timeseries/public/index.ts rename to src/plugins/vis_type_timeseries/public/index.ts index 16b099ba16ae9..fbf4a81b6ad1b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/index.ts +++ b/src/plugins/vis_type_timeseries/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from '../../../core/public'; import { MetricsPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts similarity index 92% rename from src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts rename to src/plugins/vis_type_timeseries/public/metrics_fn.ts index 1f9cbecc2a354..008b13cce6565 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -19,12 +19,8 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from '../../../../plugins/expressions/public'; -import { PersistedState } from '../../../../plugins/visualizations/public'; +import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; +import { PersistedState } from '../../visualizations/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts similarity index 85% rename from src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts rename to src/plugins/vis_type_timeseries/public/metrics_type.ts index 1db35c406eb13..f83881a619a59 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -21,11 +21,10 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; +import { EditorController } from './application'; // @ts-ignore -import { EditorController } from './editor_controller'; -// @ts-ignore -import { PANEL_TYPES } from '../../../../plugins/vis_type_timeseries/common/panel_types'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { PANEL_TYPES } from '../common/panel_types'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; export const metricsVisDefinition = { name: 'metrics', @@ -69,12 +68,9 @@ export const metricsVisDefinition = { show_legend: 1, show_grid: 1, }, - component: require('./components/vis_editor').VisEditor, + component: require('./application/components/vis_editor').VisEditor, }, editor: EditorController, - editorConfig: { - component: require('./components/vis_editor').VisEditor, - }, options: { showQueryBar: false, showFilterBar: false, diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts rename to src/plugins/vis_type_timeseries/public/plugin.ts index 0310ecf6cfd87..e6c92c31450e7 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -16,9 +16,12 @@ * specific language governing permissions and limitations * under the License. */ + +import './application/index.scss'; + import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; import { createMetricsFn } from './metrics_fn'; import { metricsVisDefinition } from './metrics_type'; @@ -30,7 +33,7 @@ import { setCoreStart, setDataStart, } from './services'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; +import { DataPublicPluginStart } from '../../data/public'; /** @internal */ export interface MetricsPluginSetupDependencies { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js b/src/plugins/vis_type_timeseries/public/request_handler.js similarity index 95% rename from src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js rename to src/plugins/vis_type_timeseries/public/request_handler.js index 2cac1567a6eb7..bd6c6d9553930 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js +++ b/src/plugins/vis_type_timeseries/public/request_handler.js @@ -17,8 +17,7 @@ * under the License. */ -import { validateInterval } from './lib/validate_interval'; -import { getTimezone } from './lib/get_timezone'; +import { getTimezone, validateInterval } from './application'; import { getUISettings, getDataStart, getCoreStart } from './services'; export const metricsRequestHandler = async ({ diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_type_timeseries/public/services.ts similarity index 90% rename from src/legacy/core_plugins/vis_type_timeseries/public/services.ts rename to src/plugins/vis_type_timeseries/public/services.ts index 64ee897247b89..d93a376584eac 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts +++ b/src/plugins/vis_type_timeseries/public/services.ts @@ -18,8 +18,8 @@ */ import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json new file mode 100644 index 0000000000000..ca02da45e9112 --- /dev/null +++ b/src/plugins/vis_type_xy/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "visTypeXy", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["charts", "expressions", "visualizations"] +} diff --git a/src/legacy/core_plugins/vis_type_xy/public/index.ts b/src/plugins/vis_type_xy/public/index.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_xy/public/index.ts rename to src/plugins/vis_type_xy/public/index.ts index 218dc8aa8a683..9af75ce9059e9 100644 --- a/src/legacy/core_plugins/vis_type_xy/public/index.ts +++ b/src/plugins/vis_type_xy/public/index.ts @@ -17,9 +17,11 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from '../../../core/public'; import { VisTypeXyPlugin as Plugin } from './plugin'; +export { VisTypeXyPluginSetup } from './plugin'; + export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } diff --git a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts similarity index 85% rename from src/legacy/core_plugins/vis_type_xy/public/plugin.ts rename to src/plugins/vis_type_xy/public/plugin.ts index ab01b6b3153fb..667018c1e6e30 100644 --- a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -25,18 +25,18 @@ import { PluginInitializerContext, } from 'kibana/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../charts/public'; export interface VisTypeXyDependencies { uiSettings: IUiSettingsClient; charts: ChartsPluginSetup; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisTypeXyPluginSetup {} + /** @internal */ export interface VisTypeXyPluginSetupDependencies { expressions: ReturnType; @@ -53,7 +53,7 @@ export interface VisTypeXyPluginStartDependencies { type VisTypeXyCoreSetup = CoreSetup; /** @internal */ -export class VisTypeXyPlugin implements Plugin { +export class VisTypeXyPlugin implements Plugin { constructor(public initializerContext: PluginInitializerContext) {} public async setup( @@ -77,6 +77,8 @@ export class VisTypeXyPlugin implements Plugin { visTypeDefinitions.forEach((vis: any) => visualizations.createBaseVisualization(vis(visualizationDependencies)) ); + + return {}; } public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { diff --git a/src/plugins/vis_type_xy/server/index.ts b/src/plugins/vis_type_xy/server/index.ts new file mode 100644 index 0000000000000..afc879dc9c845 --- /dev/null +++ b/src/plugins/vis_type_xy/server/index.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 { schema } from '@kbn/config-schema'; + +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/test/examples/embeddables/adding_children.ts b/test/examples/embeddables/adding_children.ts index 110b8ce573332..5fe88b5dd33f0 100644 --- a/test/examples/embeddables/adding_children.ts +++ b/test/examples/embeddables/adding_children.ts @@ -23,6 +23,7 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services // eslint-disable-next-line import/no-default-export export default function({ getService }: PluginFunctionalProviderContext) { const testSubjects = getService('testSubjects'); + const flyout = getService('flyout'); describe('creating and adding children', () => { before(async () => { @@ -39,5 +40,15 @@ export default function({ getService }: PluginFunctionalProviderContext) { const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task']); }); + + it('Can add a child backed off a saved object', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); + await testSubjects.click('savedObjectTitleGarbage'); + await testSubjects.moveMouseTo('euiFlyoutCloseButton'); + await flyout.ensureClosed('dashboardAddPanel'); + const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); + expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task', 'Take the garbage out']); + }); }); } diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index f815c505a8c27..eeef3333aab0f 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -66,9 +66,8 @@ export default function({ getService, getPageObjects }) { }); it('should visualize monthly data with different day intervals', async () => { - //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 - const fromTime = '2017-11-01 00:00:00.000'; - const toTime = '2018-03-21 00:00:00.000'; + const fromTime = 'Nov 01, 2017 @ 00:00:00.000'; + const toTime = 'Mar 21, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.setChartInterval('Monthly'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -76,24 +75,25 @@ export default function({ getService, getPageObjects }) { expect(chartCanvasExist).to.be(true); }); it('should visualize weekly data with within DST changes', async () => { - //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 - const fromTime = '2018-03-01 00:00:00.000'; - const toTime = '2018-05-01 00:00:00.000'; + const fromTime = 'Mar 01, 2018 @ 00:00:00.000'; + const toTime = 'May 01, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.setChartInterval('Weekly'); await PageObjects.header.waitUntilLoadingHasFinished(); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); }); - it('should visualize monthly data with different years Scaled to 30d', async () => { - //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 - const fromTime = '2010-01-01 00:00:00.000'; - const toTime = '2018-03-21 00:00:00.000'; + it('should visualize monthly data with different years Scaled to 30 days', async () => { + const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; + const toTime = 'Mar 21, 2019 @ 00:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.setChartInterval('Daily'); await PageObjects.header.waitUntilLoadingHasFinished(); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); + const chartIntervalScaledDesc = await PageObjects.discover.getChartIntervalScaledToDesc(); + expect(chartIntervalScaledDesc).to.be('Scaled to 30 days'); }); }); } diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 3cdaccf32cdc3..76f3a3aea365f 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -147,6 +147,25 @@ export default function({ getService, getPageObjects }) { await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); expect(await queryBar.getQueryString()).to.eql(''); }); + + // https://github.com/elastic/kibana/issues/63505 + it('allows clearing if non default language was remembered in localstorage', async () => { + await queryBar.switchQueryLanguage('lucene'); + await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url + await queryBar.expectQueryLanguageOrFail('lucene'); // make sure lucene is remembered after refresh (comes from localstorage) + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.expectQueryLanguageOrFail('kql'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await queryBar.expectQueryLanguageOrFail('lucene'); + }); + + // fails: bug in discover https://github.com/elastic/kibana/issues/63561 + // unskip this test when bug is fixed + it.skip('changing language removes saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.switchQueryLanguage('lucene'); + expect(await queryBar.getQueryString()).to.eql(''); + }); }); }); } diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 00bf87621864a..e43a774940391 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -162,6 +162,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return selectedOption.getVisibleText(); } + public async getChartIntervalScaledToDesc() { + await header.waitUntilLoadingHasFinished(); + return await testSubjects.getVisibleText('discoverIntervalSelectScaledToDesc'); + } + public async setChartInterval(interval: string) { const optionElement = await find.byCssSelector(`option[label="${interval}"]`, 5000); await optionElement.click(); diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index ace8b97155c09..7c7fd2d81f170 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -17,6 +17,7 @@ * under the License. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function QueryBarProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -25,6 +26,7 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont const log = getService('log'); const PageObjects = getPageObjects(['header', 'common']); const find = getService('find'); + const browser = getService('browser'); class QueryBar { async getQueryString(): Promise { @@ -62,6 +64,24 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont public async clickQuerySubmitButton(): Promise { await testSubjects.click('querySubmitButton'); } + + public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { + await testSubjects.click('switchQueryLanguageButton'); + const kqlToggle = await testSubjects.find('languageToggle'); + const currentLang = + (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; + if (lang !== currentLang) { + await kqlToggle.click(); + } + + await browser.pressKeys(browser.keys.ESCAPE); // close popover + await this.expectQueryLanguageOrFail(lang); // make sure lang is switched + } + + public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { + const queryLanguageButton = await testSubjects.find('switchQueryLanguageButton'); + expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); + } } return new QueryBar(); diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts b/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts index b0f1219a815a3..faa774b8485b1 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts @@ -26,6 +26,8 @@ export const createSamplePanelLink = (): Action => createAction({ type: SAMPLE_PANEL_LINK, getDisplayName: () => 'Sample panel Link', - execute: async () => {}, - getHref: () => 'https://example.com/kibana/test', + execute: async () => { + window.location.href = 'https://example.com/kibana/test'; + }, + getHref: async () => 'https://example.com/kibana/test', }); 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/np_ready/public/app/dashboard_input.ts index bb8951680be35..37ef8cad948cb 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/np_ready/public/app/dashboard_input.ts @@ -47,7 +47,7 @@ export const dashboardInput: DashboardContainerInput = { explicitInput: { id: '2', firstName: 'Sue', - } as any, + }, }, '822cd0f0-ce7c-419d-aeaa-1171cf452745': { gridData: { @@ -60,8 +60,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'visualization', explicitInput: { id: '822cd0f0-ce7c-419d-aeaa-1171cf452745', + savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', }, - savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', }, '66f0a265-7b06-4974-accd-d05f74f7aa82': { gridData: { @@ -74,8 +74,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'visualization', explicitInput: { id: '66f0a265-7b06-4974-accd-d05f74f7aa82', + savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', }, - savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', }, 'b2861741-40b9-4dc8-b82b-080c6e29a551': { gridData: { @@ -88,8 +88,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'search', explicitInput: { id: 'b2861741-40b9-4dc8-b82b-080c6e29a551', + savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', }, - savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', }, }, isFullScreenMode: false, diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b3744f7cf93ab..50f36ddd21c97 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -21,7 +21,7 @@ "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", - "xpack.lens": "legacy/plugins/lens", + "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", "xpack.logstash": ["plugins/logstash", "legacy/plugins/logstash"], diff --git a/x-pack/index.js b/x-pack/index.js index 3126dc17a7107..61fd4f1752316 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -29,7 +29,6 @@ import { uptime } from './legacy/plugins/uptime'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; import { actions } from './legacy/plugins/actions'; import { alerting } from './legacy/plugins/alerting'; -import { lens } from './legacy/plugins/lens'; import { ingestManager } from './legacy/plugins/ingest_manager'; import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; @@ -58,7 +57,6 @@ module.exports = function(kibana) { upgradeAssistant(kibana), uptime(kibana), encryptedSavedObjects(kibana), - lens(kibana), actions(kibana), alerting(kibana), ingestManager(kibana), diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index e4b656ae8160d..53c86f92ee557 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -211,7 +211,9 @@ export function Cytoscape({ resetConnectedEdgeStyle(event.target); }; const unselectHandler: cytoscape.EventHandler = event => { - resetConnectedEdgeStyle(); + resetConnectedEdgeStyle( + serviceName ? event.cy.getElementById(serviceName) : undefined + ); }; const debugHandler: cytoscape.EventHandler = event => { const debugEnabled = sessionStorage.getItem('apm_debug') === 'true'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 52263878ca915..491ebdc5aad15 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -27,6 +27,30 @@ interface ContentsProps { selectedNodeServiceName: string; } +// IE 11 does not handle flex properties as expected. With browser detection, +// we can use regular div elements to render contents that are almost identical. +// +// This method of detecting IE is from a Stack Overflow answer: +// https://stackoverflow.com/a/21825207 +// +// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; + +const FlexColumnGroup = (props: { + children: React.ReactNode; + style: React.CSSProperties; + direction: 'column'; + gutterSize: 's'; +}) => { + if (isIE11) { + const { direction, gutterSize, ...rest } = props; + return
; + } + return ; +}; +const FlexColumnItem = (props: { children: React.ReactNode }) => + isIE11 ?
: ; + export function Contents({ selectedNodeData, isService, @@ -36,18 +60,18 @@ export function Contents({ }: ContentsProps) { const frameworkName = selectedNodeData[SERVICE_FRAMEWORK_NAME]; return ( - - +

{label}

-
- + + {isService ? ( )} - + {isService && ( )} -
+ ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 92f66f698f044..bf0e052b951ae 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -12,6 +12,17 @@ import { } from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; import { defaultIcon, iconForNode } from './icons'; +// IE 11 does not properly load some SVGs or draw certain shapes. This causes +// a runtime error and the map fails work at all. We would prefer to do some +// kind of feature detection rather than browser detection, but some of these +// limitations are not well documented for older browsers. +// +// This method of detecting IE is from a Stack Overflow answer: +// https://stackoverflow.com/a/21825207 +// +// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; + export const animationOptions: cytoscape.AnimationOptions = { duration: parseInt(theme.euiAnimSpeedNormal, 10), // @ts-ignore The cubic-bezier options here are not recognized by the cytoscape types @@ -37,8 +48,9 @@ const style: cytoscape.Stylesheet[] = [ // used here. // // @ts-ignore - 'background-image': (el: cytoscape.NodeSingular) => - iconForNode(el) ?? defaultIcon, + 'background-image': isIE11 + ? undefined + : (el: cytoscape.NodeSingular) => iconForNode(el) ?? defaultIcon, 'background-height': (el: cytoscape.NodeSingular) => isService(el) ? '60%' : '40%', 'background-width': (el: cytoscape.NodeSingular) => @@ -65,7 +77,7 @@ const style: cytoscape.Stylesheet[] = [ 'min-zoomed-font-size': parseInt(theme.euiSizeL, 10), 'overlay-opacity': 0, shape: (el: cytoscape.NodeSingular) => - isService(el) ? 'ellipse' : 'diamond', + isService(el) ? (isIE11 ? 'rectangle' : 'ellipse') : 'diamond', 'text-background-color': theme.euiColorLightestShade, 'text-background-opacity': 0, 'text-background-padding': theme.paddingSizes.xs, @@ -87,12 +99,12 @@ const style: cytoscape.Stylesheet[] = [ 'line-color': lineColor, 'overlay-opacity': 0, 'target-arrow-color': lineColor, - 'target-arrow-shape': 'triangle', + 'target-arrow-shape': isIE11 ? 'none' : 'triangle', // The DefinitelyTyped definitions don't specify this property since it's // fairly new. // // @ts-ignore - 'target-distance-from-node': theme.paddingSizes.xs, + 'target-distance-from-node': isIE11 ? undefined : theme.paddingSizes.xs, width: 1, 'source-arrow-shape': 'none', 'z-index': zIndexEdge @@ -101,12 +113,16 @@ const style: cytoscape.Stylesheet[] = [ { selector: 'edge[bidirectional]', style: { - 'source-arrow-shape': 'triangle', + 'source-arrow-shape': isIE11 ? 'none' : 'triangle', 'source-arrow-color': lineColor, - 'target-arrow-shape': 'triangle', + 'target-arrow-shape': isIE11 ? 'none' : 'triangle', // @ts-ignore - 'source-distance-from-node': parseInt(theme.paddingSizes.xs, 10), - 'target-distance-from-node': parseInt(theme.paddingSizes.xs, 10) + 'source-distance-from-node': isIE11 + ? undefined + : parseInt(theme.paddingSizes.xs, 10), + 'target-distance-from-node': isIE11 + ? undefined + : parseInt(theme.paddingSizes.xs, 10) } }, // @ts-ignore DefinitelyTyped says visibility is "none" but it's diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index 79b3918fef99b..f75b3b427c41b 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -104,8 +104,9 @@ export const initializeCanvas = async ( href: getDocumentationLinks().canvas, }, ], - content: domNode => () => { + content: domNode => { ReactDOM.render(, domNode); + return () => ReactDOM.unmountComponentAtNode(domNode); }, }); diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts deleted file mode 100644 index e9a901c58cd90..0000000000000 --- a/x-pack/legacy/plugins/lens/index.ts +++ /dev/null @@ -1,38 +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 * as Joi from 'joi'; -import { resolve } from 'path'; -import { LegacyPluginInitializer } from 'src/legacy/types'; -import { PLUGIN_ID, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../plugins/lens/common'; - -export const lens: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - id: PLUGIN_ID, - configPrefix: `xpack.${PLUGIN_ID}`, - // task_manager could be required, but is only used for telemetry - require: ['kibana', 'elasticsearch', 'xpack_main', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - - uiExports: { - app: { - title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - description: 'Explore and visualize data.', - main: `plugins/${PLUGIN_ID}/redirect`, - listed: false, - }, - visualize: [`plugins/${PLUGIN_ID}/legacy`], - embeddableFactories: [`plugins/${PLUGIN_ID}/legacy`], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - - config: () => { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - }); -}; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/app_plugin/_index.scss deleted file mode 100644 index 2ac86f0e58a61..0000000000000 --- a/x-pack/legacy/plugins/lens/public/app_plugin/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './app'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss deleted file mode 100644 index 99c357b53952f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './visualization'; diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/_index.scss b/x-pack/legacy/plugins/lens/public/drag_drop/_index.scss deleted file mode 100644 index 1b3d0cf0a3c2a..0000000000000 --- a/x-pack/legacy/plugins/lens/public/drag_drop/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './drag_drop' diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss deleted file mode 100644 index 4d7e054ff03c3..0000000000000 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './editor_frame/index'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss deleted file mode 100644 index 6c6a63c8c7eb6..0000000000000 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import './chart_switch'; -@import './config_panel_wrapper'; -@import './data_panel_wrapper'; -@import './expression_renderer'; -@import './frame_layout'; -@import './suggestion_panel'; -@import './workspace_panel_wrapper'; - diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss deleted file mode 100644 index a283198d6cf73..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './datapanel'; -@import './field_item'; - -@import './dimension_panel/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss deleted file mode 100644 index 26f805fe735f0..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './field_select'; -@import './popover'; diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts deleted file mode 100644 index 3b7b6a7a1b510..0000000000000 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ /dev/null @@ -1,17 +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 { npSetup, npStart } from 'ui/new_platform'; - -export * from './types'; - -import { plugin } from './index'; - -const pluginInstance = plugin(); -pluginInstance.setup(npSetup.core, { - ...npSetup.plugins, -}); -pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/lens/public/redirect.ts b/x-pack/legacy/plugins/lens/public/redirect.ts deleted file mode 100644 index 25b0188214c5e..0000000000000 --- a/x-pack/legacy/plugins/lens/public/redirect.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. - */ - -// This file redirects lens urls starting with app/lens#... to their counterpart on app/kibana#lens/... to -// make sure it's compatible with the 7.5 release - -import { npSetup } from 'ui/new_platform'; -import chrome from 'ui/chrome'; - -chrome.setRootController('lens', () => { - // prefix the path in the hash with lens/ - const prefixedHashRoute = window.location.hash.replace(/^#\//, '#/lens/'); - - // redirect to the new lens url `app/kibana#/lens/...` - window.location.href = npSetup.core.http.basePath.prepend('/app/kibana' + prefixedHashRoute); -}); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss deleted file mode 100644 index 794ed4aed82ec..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './_xy_expression'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss index 249b6dfca5c76..f9ad412f7e48a 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss @@ -1,3 +1,3 @@ .mapStyleSettings__fixedBox { - width: $euiSize * 7.5; -} \ No newline at end of file + width: $euiSize * 7.5; +} diff --git a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.ts.snap b/x-pack/legacy/plugins/reporting/__snapshots__/index.test.ts.snap deleted file mode 100644 index 3ae3079da136b..0000000000000 --- a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,387 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`config schema with context {"dev":false,"dist":false} produces correct config 1`] = ` -Object { - "capture": Object { - "browser": Object { - "autoDownload": true, - "chromium": Object { - "disableSandbox": "", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 1, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; - -exports[`config schema with context {"dev":false,"dist":true} produces correct config 1`] = ` -Object { - "capture": Object { - "browser": Object { - "autoDownload": false, - "chromium": Object { - "disableSandbox": "", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 3, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; - -exports[`config schema with context {"dev":true,"dist":false} produces correct config 1`] = ` -Object { - "capture": Object { - "browser": Object { - "autoDownload": true, - "chromium": Object { - "disableSandbox": "", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 1, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; - -exports[`config schema with context {"dev":true,"dist":true} produces correct config 1`] = ` -Object { - "capture": Object { - "browser": Object { - "autoDownload": false, - "chromium": Object { - "disableSandbox": "", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 3, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; diff --git a/x-pack/legacy/plugins/reporting/config.ts b/x-pack/legacy/plugins/reporting/config.ts deleted file mode 100644 index 5eceb84c83e43..0000000000000 --- a/x-pack/legacy/plugins/reporting/config.ts +++ /dev/null @@ -1,183 +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 { BROWSER_TYPE } from './common/constants'; -// @ts-ignore untyped module -import { config as appConfig } from './server/config/config'; -import { getDefaultChromiumSandboxDisabled } from './server/browsers'; - -export async function config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - kibanaServer: Joi.object({ - protocol: Joi.string().valid(['http', 'https']), - hostname: Joi.string().invalid('0'), - port: Joi.number().integer(), - }).default(), - queue: Joi.object({ - indexInterval: Joi.string().default('week'), - pollEnabled: Joi.boolean().default(true), - pollInterval: Joi.number() - .integer() - .default(3000), - pollIntervalErrorMultiplier: Joi.number() - .integer() - .default(10), - timeout: Joi.number() - .integer() - .default(120000), - }).default(), - capture: Joi.object({ - timeouts: Joi.object({ - openUrl: Joi.number() - .integer() - .default(30000), - waitForElements: Joi.number() - .integer() - .default(30000), - renderComplete: Joi.number() - .integer() - .default(30000), - }).default(), - networkPolicy: Joi.object({ - enabled: Joi.boolean().default(true), - rules: Joi.array() - .items( - Joi.object({ - allow: Joi.boolean().required(), - protocol: Joi.string(), - host: Joi.string(), - }) - ) - .default([ - { allow: true, protocol: 'http:' }, - { allow: true, protocol: 'https:' }, - { allow: true, protocol: 'ws:' }, - { allow: true, protocol: 'wss:' }, - { allow: true, protocol: 'data:' }, - { allow: false }, // Default action is to deny! - ]), - }).default(), - zoom: Joi.number() - .integer() - .default(2), - viewport: Joi.object({ - width: Joi.number() - .integer() - .default(1950), - height: Joi.number() - .integer() - .default(1200), - }).default(), - timeout: Joi.number() - .integer() - .default(20000), // deprecated - loadDelay: Joi.number() - .integer() - .default(3000), - settleTime: Joi.number() - .integer() - .default(1000), // deprecated - concurrency: Joi.number() - .integer() - .default(appConfig.concurrency), // deprecated - browser: Joi.object({ - type: Joi.any() - .valid(BROWSER_TYPE) - .default(BROWSER_TYPE), - autoDownload: Joi.boolean().when('$dist', { - is: true, - then: Joi.default(false), - otherwise: Joi.default(true), - }), - chromium: Joi.object({ - inspect: Joi.boolean() - .when('$dev', { - is: false, - then: Joi.valid(false), - else: Joi.default(false), - }) - .default(), - disableSandbox: Joi.boolean().default(await getDefaultChromiumSandboxDisabled()), - proxy: Joi.object({ - enabled: Joi.boolean().default(false), - server: Joi.string() - .uri({ scheme: ['http', 'https'] }) - .when('enabled', { - is: Joi.valid(false), - then: Joi.valid(null), - else: Joi.required(), - }), - bypass: Joi.array() - .items(Joi.string().regex(/^[^\s]+$/)) - .when('enabled', { - is: Joi.valid(false), - then: Joi.valid(null), - else: Joi.default([]), - }), - }).default(), - maxScreenshotDimension: Joi.number() - .integer() - .default(1950), - }).default(), - }).default(), - maxAttempts: Joi.number() - .integer() - .greater(0) - .when('$dist', { - is: true, - then: Joi.default(3), - otherwise: Joi.default(1), - }) - .default(), - }).default(), - csv: Joi.object({ - useByteOrderMarkEncoding: Joi.boolean().default(false), - checkForFormulas: Joi.boolean().default(true), - enablePanelActionDownload: Joi.boolean().default(true), - maxSizeBytes: Joi.number() - .integer() - .default(1024 * 1024 * 10), // bytes in a kB * kB in a mB * 10 - scroll: Joi.object({ - duration: Joi.string() - .regex(/^[0-9]+(d|h|m|s|ms|micros|nanos)$/, { name: 'DurationString' }) - .default('30s'), - size: Joi.number() - .integer() - .default(500), - }).default(), - }).default(), - encryptionKey: Joi.when(Joi.ref('$dist'), { - is: true, - then: Joi.string(), - otherwise: Joi.string().default('a'.repeat(32)), - }), - roles: Joi.object({ - allow: Joi.array() - .items(Joi.string()) - .default(['reporting_user']), - }).default(), - index: Joi.string().default('.reporting'), - poll: Joi.object({ - jobCompletionNotifier: Joi.object({ - interval: Joi.number() - .integer() - .default(10000), - intervalErrorMultiplier: Joi.number() - .integer() - .default(5), - }).default(), - jobsRefresh: Joi.object({ - interval: Joi.number() - .integer() - .default(5000), - intervalErrorMultiplier: Joi.number() - .integer() - .default(5), - }).default(), - }).default(), - }).default(); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts index 2c43517dbcaa9..5cd2f3e636a93 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts @@ -54,7 +54,7 @@ export abstract class Layout { public abstract getPdfPageSize(pageSizeParams: PageSizeParams): string | Size; - public abstract getViewport(itemsCount: number): ViewZoomWidthHeight; + public abstract getViewport(itemsCount: number): ViewZoomWidthHeight | null; public abstract getBrowserZoom(): number; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts index 75ac3dca4ffa0..68d660257a56d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts @@ -22,6 +22,7 @@ import { LevelLogger } from '../../../../server/lib'; import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../../../test_helpers'; import { ConditionalHeaders, HeadlessChromiumDriver } from '../../../../types'; import { CaptureConfig } from '../../../../server/types'; +import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; import { ElementsPositionAndAttribute } from './types'; @@ -57,10 +58,30 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": "Default ", "title": "Default Mock Title", }, @@ -95,6 +116,26 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { @@ -106,6 +147,26 @@ describe('Screenshot Observable Pipeline', () => { "timeRange": "Default GetTimeRange Result", }, Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { @@ -150,10 +211,27 @@ describe('Screenshot Observable Pipeline', () => { await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -161,10 +239,27 @@ describe('Screenshot Observable Pipeline', () => { "timeRange": null, }, Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -208,10 +303,27 @@ describe('Screenshot Observable Pipeline', () => { await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": "Instant timeout has fired!", "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -221,5 +333,69 @@ describe('Screenshot Observable Pipeline', () => { ] `); }); + + it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { + // mocks + const mockBrowserEvaluate = jest.fn(); + mockBrowserEvaluate.mockImplementation(() => { + const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1; + const { context: mockCall } = mockBrowserEvaluate.mock.calls[lastCallIndex][1]; + + if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) { + return Promise.resolve(null); + } else { + return Promise.resolve(); + } + }); + mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + evaluate: mockBrowserEvaluate, + }); + mockLayout.getViewport = () => null; + + // test + const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshot = async () => { + return await getScreenshots$({ + logger, + urls: ['/welcome/home/start/index.php3?page=./home.php3'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); + }; + + await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 1200, + "left": 0, + "top": 0, + "width": 1800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": undefined, + }, + ] + `); + }); }); }); 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 53a11c18abd79..519a3289395b9 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 @@ -18,6 +18,9 @@ import { ScreenSetupData, ScreenshotObservableOpts, ScreenshotResults } from './ import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; +const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; +const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; + export function screenshotsObservableFactory( captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory @@ -42,7 +45,7 @@ export function screenshotsObservableFactory( mergeMap(() => openUrl(captureConfig, driver, url, conditionalHeaders, logger)), mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), mergeMap(async itemsCount => { - const viewport = layout.getViewport(itemsCount); + const viewport = layout.getViewport(itemsCount) || getDefaultViewPort(); await Promise.all([ driver.setViewport(viewport, logger), waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), @@ -83,7 +86,12 @@ export function screenshotsObservableFactory( : getDefaultElementPosition(layout.getViewport(1)); const screenshots = await getScreenshots(driver, elements, logger); const { timeRange, error: setupError } = data; - return { timeRange, screenshots, error: setupError }; + return { + timeRange, + screenshots, + error: setupError, + elementsPositionAndAttributes: elements, + }; } ) ); @@ -97,17 +105,30 @@ export function screenshotsObservableFactory( }; } +/* + * If Kibana is showing a non-HTML error message, the viewport might not be + * provided by the browser. + */ +const getDefaultViewPort = () => ({ + height: DEFAULT_SCREENSHOT_CLIP_HEIGHT, + width: DEFAULT_SCREENSHOT_CLIP_WIDTH, + zoom: 1, +}); /* * If an error happens setting up the page, we don't know if there actually * are any visualizations showing. These defaults should help capture the page * enough for the user to see the error themselves */ -const getDefaultElementPosition = ({ height, width }: { height: number; width: number }) => [ - { +const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { + const height = dimensions?.height || DEFAULT_SCREENSHOT_CLIP_HEIGHT; + const width = dimensions?.width || DEFAULT_SCREENSHOT_CLIP_WIDTH; + + const defaultObject = { position: { boundingClientRect: { top: 0, left: 0, height, width }, scroll: { x: 0, y: 0 }, }, attributes: {}, - }, -]; + }; + return [defaultObject]; +}; 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 76613c2d631d6..e113a5d228cd7 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 @@ -45,4 +45,5 @@ export interface ScreenshotResults { timeRange: TimeRange | null; screenshots: Screenshot[]; error?: Error; + elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing } diff --git a/x-pack/legacy/plugins/reporting/index.test.ts b/x-pack/legacy/plugins/reporting/index.test.ts deleted file mode 100644 index 8148adab67874..0000000000000 --- a/x-pack/legacy/plugins/reporting/index.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { reporting } from './index'; -import { getConfigSchema } from '../../../test_utils'; - -// The snapshot records the number of cpus available -// to make the snapshot deterministic `os.cpus` needs to be mocked -// but the other members on `os` must remain untouched -jest.mock('os', () => { - const os = jest.requireActual('os'); - os.cpus = () => [{}, {}, {}, {}]; - return os; -}); - -// eslint-disable-next-line jest/valid-describe -const describeWithContext = describe.each([ - [{ dev: false, dist: false }], - [{ dev: true, dist: false }], - [{ dev: false, dist: true }], - [{ dev: true, dist: true }], -]); - -describeWithContext('config schema with context %j', context => { - it('produces correct config', async () => { - const schema = await getConfigSchema(reporting); - const value: any = await schema.validate({}, { context }); - value.capture.browser.chromium.disableSandbox = ''; - await expect(value).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index a5d27d0545da1..fb95e2c2edc24 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; import { resolve } from 'path'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; -import { config as reportingConfig } from './config'; import { legacyInit } from './server/legacy'; import { ReportingPluginSpecOptions } from './types'; @@ -17,10 +16,8 @@ const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6); export const reporting = (kibana: any) => { return new kibana.Plugin({ id: PLUGIN_ID, - configPrefix: 'xpack.reporting', publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - config: reportingConfig, uiExports: { uiSettingDefaults: { @@ -47,14 +44,5 @@ export const reporting = (kibana: any) => { async init(server: Legacy.Server) { return legacyInit(server, this); }, - - deprecations({ unused }: any) { - return [ - unused('capture.concurrency'), - unused('capture.timeout'), - unused('capture.settleTime'), - unused('kibanaApp'), - ]; - }, } as ReportingPluginSpecOptions); }; diff --git a/x-pack/legacy/plugins/reporting/log_configuration.ts b/x-pack/legacy/plugins/reporting/log_configuration.ts deleted file mode 100644 index 7aaed2038bd52..0000000000000 --- a/x-pack/legacy/plugins/reporting/log_configuration.ts +++ /dev/null @@ -1,35 +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 getosSync, { LinuxOs } from 'getos'; -import { promisify } from 'util'; -import { BROWSER_TYPE } from './common/constants'; -import { CaptureConfig } from './server/types'; -import { Logger } from './types'; - -const getos = promisify(getosSync); - -export async function logConfiguration(captureConfig: CaptureConfig, logger: Logger) { - const { - browser: { - type: browserType, - chromium: { disableSandbox }, - }, - } = captureConfig; - - logger.debug(`Browser type: ${browserType}`); - if (browserType === BROWSER_TYPE) { - logger.debug(`Chromium sandbox disabled: ${disableSandbox}`); - } - - const os = await getos(); - const { os: osName, dist, release } = os as LinuxOs; - if (dist) { - logger.debug(`Running on os "${osName}", distribution "${dist}", release "${release}"`); - } else { - logger.debug(`Running on os "${osName}"`); - } -} diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 4b80e129c04da..dfaa87021c31c 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -190,19 +190,14 @@ export class HeadlessChromiumDriver { } public async screenshot(elementPosition: ElementPosition): Promise { - let clip; - if (elementPosition) { - const { boundingClientRect, scroll = { x: 0, y: 0 } } = elementPosition; - clip = { + const { boundingClientRect, scroll } = elementPosition; + const screenshot = await this.page.screenshot({ + clip: { x: boundingClientRect.left + scroll.x, y: boundingClientRect.top + scroll.y, height: boundingClientRect.height, width: boundingClientRect.width, - }; - } - - const screenshot = await this.page.screenshot({ - clip, + }, }); return screenshot.toString('base64'); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts index a2f7a1f3ad0da..928f3b8377809 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts @@ -10,7 +10,7 @@ type ViewportConfig = CaptureConfig['viewport']; type BrowserConfig = CaptureConfig['browser']['chromium']; interface LaunchArgs { - userDataDir: BrowserConfig['userDataDir']; + userDataDir: string; viewport: ViewportConfig; disableSandbox: BrowserConfig['disableSandbox']; proxy: BrowserConfig['proxy']; diff --git a/x-pack/legacy/plugins/reporting/server/browsers/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/index.ts index 1e42e2736962e..7f902c84308f6 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/index.ts @@ -8,7 +8,6 @@ import * as chromiumDefinition from './chromium'; export { ensureAllBrowsersDownloaded } from './download'; export { createBrowserDriverFactory } from './create_browser_driver_factory'; -export { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; export { HeadlessChromiumDriver } from './chromium/driver'; export { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; diff --git a/x-pack/legacy/plugins/reporting/server/config/index.ts b/x-pack/legacy/plugins/reporting/server/config/index.ts index b7b67b57932eb..c6b915be3a94a 100644 --- a/x-pack/legacy/plugins/reporting/server/config/index.ts +++ b/x-pack/legacy/plugins/reporting/server/config/index.ts @@ -6,10 +6,9 @@ import { Legacy } from 'kibana'; import { CoreSetup } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; -import crypto from 'crypto'; import { get } from 'lodash'; -import { NetworkPolicy } from '../../types'; +import { ConfigType as ReportingConfigType } from '../../../../../plugins/reporting/server'; +export { ReportingConfigType }; // make config.get() aware of the value type it returns interface Config { @@ -56,131 +55,6 @@ export interface ReportingConfig extends Config { kbnConfig: Config; } -type BrowserType = 'chromium'; - -interface BrowserConfig { - inspect: boolean; - userDataDir: string; - viewport: { width: number; height: number }; - disableSandbox: boolean; - proxy: { - enabled: boolean; - server?: string; - bypass?: string[]; - }; -} - -interface CaptureConfig { - browser: { - type: BrowserType; - autoDownload: boolean; - chromium: BrowserConfig; - }; - maxAttempts: number; - networkPolicy: NetworkPolicy; - loadDelay: number; - timeouts: { - openUrl: number; - waitForElements: number; - renderComplete: number; - }; - viewport: any; - zoom: any; -} - -interface QueueConfig { - indexInterval: string; - pollEnabled: boolean; - pollInterval: number; - pollIntervalErrorMultiplier: number; - timeout: number; -} - -interface ScrollConfig { - duration: string; - size: number; -} - -export interface ReportingConfigType { - capture: CaptureConfig; - csv: { - scroll: ScrollConfig; - enablePanelActionDownload: boolean; - checkForFormulas: boolean; - maxSizeBytes: number; - useByteOrderMarkEncoding: boolean; - }; - encryptionKey: string; - kibanaServer: any; - index: string; - queue: QueueConfig; - roles: any; -} - -const addConfigDefaults = ( - server: Legacy.Server, - core: CoreSetup, - baseConfig: ReportingConfigType -) => { - // encryption key - let encryptionKey = baseConfig.encryptionKey; - if (encryptionKey === undefined) { - server.log( - ['reporting', 'config', 'warning'], - i18n.translate('xpack.reporting.selfCheckEncryptionKey.warning', { - defaultMessage: - `Generating a random key for {setting}. To prevent pending reports ` + - `from failing on restart, please set {setting} in kibana.yml`, - values: { - setting: 'xpack.reporting.encryptionKey', - }, - }) - ); - encryptionKey = crypto.randomBytes(16).toString('hex'); - } - - const { kibanaServer: reportingServer } = baseConfig; - const serverInfo = core.http.getServerInfo(); - - // kibanaServer.hostname, default to server.host, don't allow "0" - let kibanaServerHostname = reportingServer.hostname ? reportingServer.hostname : serverInfo.host; - if (kibanaServerHostname === '0') { - server.log( - ['reporting', 'config', 'warning'], - i18n.translate('xpack.reporting.selfCheckHostname.warning', { - defaultMessage: - `Found 'server.host: "0"' in settings. This is incompatible with Reporting. ` + - `To enable Reporting to work, '{setting}: 0.0.0.0' is being automatically to the configuration. ` + - `You can change to 'server.host: 0.0.0.0' or add '{setting}: 0.0.0.0' in kibana.yml to prevent this message.`, - values: { - setting: 'xpack.reporting.kibanaServer.hostname', - }, - }) - ); - kibanaServerHostname = '0.0.0.0'; - } - - // kibanaServer.port, default to server.port - const kibanaServerPort = reportingServer.port - ? reportingServer.port - : serverInfo.port; // prettier-ignore - - // kibanaServer.protocol, default to server.protocol - const kibanaServerProtocol = reportingServer.protocol - ? reportingServer.protocol - : serverInfo.protocol; - - return { - ...baseConfig, - encryptionKey, - kibanaServer: { - hostname: kibanaServerHostname, - port: kibanaServerPort, - protocol: kibanaServerProtocol, - }, - }; -}; - export const buildConfig = ( core: CoreSetup, server: Legacy.Server, @@ -204,10 +78,8 @@ export const buildConfig = ( }, }; - // spreading arguments as an array allows the return type to be known by the compiler - reportingConfig = addConfigDefaults(server, core, reportingConfig); return { - get: (...keys: string[]) => get(reportingConfig, keys.join('.'), null), + get: (...keys: string[]) => get(reportingConfig, keys.join('.'), null), // spreading arguments as an array allows the return type to be known by the compiler kbnConfig: { get: (...keys: string[]) => get(kbnConfig, keys.join('.'), null), }, diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts index 679b42aca6de5..d044dc866ed0e 100644 --- a/x-pack/legacy/plugins/reporting/server/legacy.ts +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -5,7 +5,9 @@ */ import { Legacy } from 'kibana'; +import { take } from 'rxjs/operators'; import { PluginInitializerContext } from 'src/core/server'; +import { PluginsSetup } from '../../../../plugins/reporting/server'; import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { ReportingPluginSpecOptions } from '../types'; import { buildConfig } from './config'; @@ -17,7 +19,6 @@ const buildLegacyDependencies = ( reportingPlugin: ReportingPluginSpecOptions ): LegacySetup => ({ route: server.route.bind(server), - config: server.config, plugins: { xpack_main: server.plugins.xpack_main, reporting: reportingPlugin, @@ -32,14 +33,13 @@ export const legacyInit = async ( reportingLegacyPlugin: ReportingPluginSpecOptions ) => { const { core: coreSetup } = server.newPlatform.setup; - const legacyConfig = server.config(); - const reportingConfig = buildConfig(coreSetup, server, legacyConfig.get('xpack.reporting')); - + const { config$ } = (server.newPlatform.setup.plugins.reporting as PluginsSetup).__legacy; + const reportingConfig = await config$.pipe(take(1)).toPromise(); const __LEGACY = buildLegacyDependencies(server, reportingLegacyPlugin); const pluginInstance = plugin( server.newPlatform.coreContext as PluginInitializerContext, - reportingConfig + buildConfig(coreSetup, server, reportingConfig) ); await pluginInstance.setup(coreSetup, { elasticsearch: coreSetup.elasticsearch, diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index ad93a1882746d..ea80a652bb506 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -760,6 +760,27 @@ describe('Worker class', function() { }); }); + it('handle warnings in the output by reflecting a warning status', () => { + const workerFn = () => { + return Promise.resolve({ + ...payload, + warnings: [`Don't run with scissors!`], + }); + }; + worker = new Worker(mockQueue, 'test', workerFn, defaultWorkerOptions); + + return worker + ._performJob({ + test: true, + ...job, + }) + .then(() => { + sinon.assert.calledOnce(updateSpy); + const doc = updateSpy.firstCall.args[1].body.doc; + expect(doc).to.have.property('status', constants.JOB_STATUS_WARNINGS); + }); + }); + it('should emit completion event', function(done) { worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.ts similarity index 89% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.js rename to x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.ts index 620a567e18fe7..7c7f1431adf23 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.ts @@ -8,6 +8,7 @@ export const statuses = { JOB_STATUS_PENDING: 'pending', JOB_STATUS_PROCESSING: 'processing', JOB_STATUS_COMPLETED: 'completed', + JOB_STATUS_WARNINGS: 'completed_with_warnings', JOB_STATUS_FAILED: 'failed', JOB_STATUS_CANCELLED: 'cancelled', }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js index 6cdbe8f968f75..ceb4ef43b2d9d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js @@ -8,6 +8,7 @@ import moment from 'moment'; export const intervals = ['year', 'month', 'week', 'day', 'hour', 'minute']; +// TODO: This helper function can be removed by using `schema.duration` objects in the reporting config schema export function indexTimestamp(intervalStr, separator = '-') { if (separator.match(/[a-z]/i)) throw new Error('Interval separator can not be a letter'); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js index 113059fa2fa47..ab0bb6740f078 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js @@ -285,8 +285,12 @@ export class Worker extends events.EventEmitter { const completedTime = moment().toISOString(); const docOutput = this._formatOutput(output); + const status = + output && output.warnings && output.warnings.length > 0 + ? constants.JOB_STATUS_WARNINGS + : constants.JOB_STATUS_COMPLETED; const doc = { - status: constants.JOB_STATUS_COMPLETED, + status, completed_at: completedTime, output: docOutput, }; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index c9ed2e81c6792..e0fa99106a93e 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -5,15 +5,12 @@ */ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; -import { logConfiguration } from '../log_configuration'; import { createBrowserDriverFactory } from './browsers'; import { ReportingCore, ReportingConfig } from './core'; import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; import { registerReportingUsageCollector } from './usage'; -// @ts-ignore no module definition -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; export class ReportingPlugin implements Plugin { @@ -61,8 +58,6 @@ export class ReportingPlugin setFieldFormats(plugins.data.fieldFormats); - logConfiguration(this.config.get('capture'), this.logger); - return {}; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index aef37754681ec..c243d0b4266ea 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -9,6 +9,7 @@ import contentDisposition from 'content-disposition'; import * as _ from 'lodash'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { ExportTypeDefinition, ExportTypesRegistry, JobDocOutput, JobSource } from '../../../types'; +import { statuses } from '../../lib/esqueue/constants/statuses'; interface ICustomHeaders { [x: string]: any; @@ -99,11 +100,11 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist const { status, jobtype: jobType, payload: { title } = { title: '' } } = doc._source; const { output } = doc._source; - if (status === 'completed') { + if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) { return getCompleted(output, jobType, title); } - if (status === 'failed') { + if (status === statuses.JOB_STATUS_FAILED) { return getFailure(output); } diff --git a/x-pack/legacy/plugins/reporting/server/types.d.ts b/x-pack/legacy/plugins/reporting/server/types.d.ts index bec00688432cc..fb77eae4e7eea 100644 --- a/x-pack/legacy/plugins/reporting/server/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/types.d.ts @@ -11,7 +11,7 @@ import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/ import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import { ReportingPluginSpecOptions } from '../types'; -import { ReportingConfig, ReportingConfigType } from './core'; +import { ReportingConfigType } from './core'; export interface ReportingSetupDeps { elasticsearch: ElasticsearchServiceSetup; @@ -30,7 +30,6 @@ export type ReportingSetup = object; export type ReportingStart = object; export interface LegacySetup { - config: Legacy.Server['config']; plugins: { xpack_main: XPackMainPlugin & { status?: any; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts index 6e95bed2ecf92..1be10f6a2056f 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts @@ -34,7 +34,7 @@ const getMockElementsPositionAndAttributes = ( ): ElementsPositionAndAttribute[] => [ { position: { - boundingClientRect: { top: 0, left: 0, width: 10, height: 11 }, + boundingClientRect: { top: 0, left: 0, width: 800, height: 600 }, scroll: { x: 0, y: 0 }, }, attributes: { title, description }, @@ -78,7 +78,7 @@ mockBrowserEvaluate.mockImplementation(() => { }); const mockScreenshot = jest.fn(); mockScreenshot.mockImplementation((item: ElementsPositionAndAttribute) => { - return Promise.resolve(`allyourBase64 of ${Object.keys(item)}`); + return Promise.resolve(`allyourBase64`); }); const getCreatePage = (driver: HeadlessChromiumDriver) => jest.fn().mockImplementation(() => Rx.of({ driver, exit$: Rx.never() })); @@ -94,31 +94,23 @@ export const createMockBrowserDriverFactory = async ( logger: Logger, opts: Partial = {} ): Promise => { - const captureConfig = { + const captureConfig: CaptureConfig = { timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, browser: { type: 'chromium', chromium: { inspect: false, disableSandbox: false, - userDataDir: '/usr/data/dir', - viewport: { width: 12, height: 12 }, proxy: { enabled: false, server: undefined, bypass: undefined }, }, autoDownload: false, - inspect: true, - userDataDir: '/usr/data/dir', - viewport: { width: 12, height: 12 }, - disableSandbox: false, - proxy: { enabled: false, server: undefined, bypass: undefined }, - maxScreenshotDimension: undefined, }, networkPolicy: { enabled: true, rules: [] }, viewport: { width: 800, height: 600 }, loadDelay: 2000, - zoom: 1, + zoom: 2, maxAttempts: 1, - } as CaptureConfig; + }; const binaryPath = '/usr/local/share/common/secure/'; const mockBrowserDriverFactory = await createDriverFactory(binaryPath, logger, captureConfig); diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts index be60b56dcc0c1..81090e7616501 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts @@ -12,7 +12,7 @@ import { CaptureConfig } from '../server/types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { id: LayoutTypes.PRESERVE_LAYOUT, - dimensions: { height: 12, width: 12 }, + dimensions: { height: 100, width: 100 }, }) as LayoutInstance; mockLayout.selectors = { renderComplete: 'renderedSelector', diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts index 34ff91d1972a0..ec00023b4d449 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts @@ -11,7 +11,6 @@ jest.mock('../server/browsers'); jest.mock('../server/lib/create_queue'); jest.mock('../server/lib/enqueue_job'); jest.mock('../server/lib/validate'); -jest.mock('../log_configuration'); import { EventEmitter } from 'events'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index eec7da7dc6733..2e7da6663ab03 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -8,6 +8,7 @@ import { EventEmitter } from 'events'; import { ResponseObject } from 'hapi'; import { Legacy } from 'kibana'; import { CallCluster } from '../../../../src/legacy/core_plugins/elasticsearch'; +import { JobStatus } from '../../../plugins/reporting'; // reporting new platform import { CancellationToken } from './common/cancellation_token'; import { ReportingCore } from './server/core'; import { LevelLogger } from './server/lib/level_logger'; @@ -150,7 +151,7 @@ export interface JobSource { jobtype: string; output: JobDocOutput; payload: JobDocPayload; - status: string; // completed, failed, etc + status: JobStatus; }; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx index adac26a8ac92b..e63bce388ae80 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx @@ -26,6 +26,7 @@ const mockLocationWithState = { state: { insertTimeline: { timelineId: 'timeline-id', + timelineSavedObjectId: '34578-3497-5893-47589-34759', timelineTitle: 'Timeline title', }, }, @@ -49,7 +50,7 @@ describe('Insert timeline popover ', () => { payload: { id: 'timeline-id', show: false }, type: 'x-pack/siem/local/timeline/SHOW_TIMELINE', }); - expect(onTimelineChange).toBeCalledWith('Timeline title', 'timeline-id'); + expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); }); it('should do nothing when router state', () => { jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx index cf1a4ebec9bb6..573e010868bab 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx @@ -23,6 +23,7 @@ interface InsertTimelinePopoverProps { interface RouterState { insertTimeline: { timelineId: string; + timelineSavedObjectId: string; timelineTitle: string; }; } @@ -46,7 +47,7 @@ export const InsertTimelinePopoverComponent: React.FC = ({ ); onTimelineChange( routerState.insertTimeline.timelineTitle, - routerState.insertTimeline.timelineId + routerState.insertTimeline.timelineSavedObjectId ); setRouterState(null); } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index 0a2ab5c9d186a..6f7e1f782d3f6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -21,6 +21,7 @@ import React, { useCallback } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; import { Note } from '../../../lib/note'; import { Notes } from '../../notes'; @@ -29,6 +30,8 @@ import { NOTES_PANEL_WIDTH } from './notes_size'; import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; import * as i18n from './translations'; import { SiemPageName } from '../../../pages/home/types'; +import { timelineSelectors } from '../../../store/timeline'; +import { State } from '../../../store'; export const historyToolTip = 'The chronological history of actions related to this timeline'; export const streamLiveToolTip = 'Update the Timeline as new data arrives'; @@ -121,6 +124,9 @@ interface NewCaseProps { export const NewCase = React.memo(({ onClosePopover, timelineId, timelineTitle }) => { const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); const handleClick = useCallback(() => { onClosePopover(); history.push({ @@ -128,6 +134,7 @@ export const NewCase = React.memo(({ onClosePopover, timelineId, t state: { insertTimeline: { timelineId, + timelineSavedObjectId: savedObjectId, timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, }, }, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts new file mode 100644 index 0000000000000..6d2cfb7147537 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + CaseUserActions, + FetchCasesProps, + SortFieldCase, +} from '../types'; +import { + actionLicenses, + allCases, + basicCase, + basicCaseCommentPatch, + basicCasePost, + casesStatus, + caseUserActions, + pushedCase, + respReporters, + serviceConnector, + tags, +} from '../mock'; +import { + CaseExternalServiceRequest, + CasePatchRequest, + CasePostRequest, + CommentRequest, + ServiceConnectorCaseParams, + ServiceConnectorCaseResponse, + User, +} from '../../../../../../../plugins/case/common/api'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => { + return Promise.resolve(basicCase); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise => + Promise.resolve(casesStatus); + +export const getTags = async (signal: AbortSignal): Promise => Promise.resolve(tags); + +export const getReporters = async (signal: AbortSignal): Promise => + Promise.resolve(respReporters); + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise => Promise.resolve(caseUserActions); + +export const getCases = async ({ + filterOptions = { + search: '', + reporters: [], + status: 'open', + tags: [], + }, + queryParams = { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise => Promise.resolve(allCases); + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise => + Promise.resolve(basicCasePost); + +export const patchCase = async ( + caseId: string, + updatedCase: Pick, + version: string, + signal: AbortSignal +): Promise => Promise.resolve([basicCase]); + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise => Promise.resolve(allCases.cases); + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal +): Promise => Promise.resolve(basicCase); + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal +): Promise => Promise.resolve(basicCaseCommentPatch); + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise => + Promise.resolve(true); + +export const pushCase = async ( + caseId: string, + push: CaseExternalServiceRequest, + signal: AbortSignal +): Promise => Promise.resolve(pushedCase); + +export const pushToService = async ( + connectorId: string, + casePushParams: ServiceConnectorCaseParams, + signal: AbortSignal +): Promise => Promise.resolve(serviceConnector); + +export const getActionLicense = async (signal: AbortSignal): Promise => + Promise.resolve(actionLicenses); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx new file mode 100644 index 0000000000000..4f5655cc9f221 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx @@ -0,0 +1,463 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { KibanaServices } from '../../lib/kibana'; +import { + deleteCases, + getActionLicense, + getCase, + getCases, + getCasesStatus, + getCaseUserActions, + getReporters, + getTags, + patchCase, + patchCasesStatus, + patchComment, + postCase, + postComment, + pushCase, + pushToService, +} from './api'; +import { + actionLicenses, + allCases, + basicCase, + allCasesSnake, + basicCaseSnake, + actionTypeExecutorResult, + pushedCaseSnake, + casesStatus, + casesSnake, + cases, + caseUserActions, + pushedCase, + pushSnake, + reporters, + respReporters, + serviceConnector, + casePushParams, + tags, + caseUserActionsSnake, + casesStatusSnake, +} from './mock'; +import { CASES_URL } from './constants'; +import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; +import * as i18n from './translations'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('deleteCases', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(''); + }); + const data = ['1', '2']; + + test('check url, method, signal', async () => { + await deleteCases(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'DELETE', + query: { ids: JSON.stringify(data) }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await deleteCases(data, abortCtrl.signal); + expect(resp).toEqual(''); + }); + }); + describe('getActionLicense', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(actionLicenses); + }); + test('check url, method, signal', async () => { + await getActionLicense(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`/api/action/types`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getActionLicense(abortCtrl.signal); + expect(resp).toEqual(actionLicenses); + }); + }); + describe('getCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = basicCase.id; + + test('check url, method, signal', async () => { + await getCase(data, true, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, { + method: 'GET', + query: { includeComments: true }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCase(data, true, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + describe('getCases', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(allCasesSnake); + }); + test('check url, method, signal', async () => { + await getCases({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters: [], + tags: [], + status: 'open', + }, + signal: abortCtrl.signal, + }); + }); + test('correctly applies filters', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + reporters: [...respReporters, { username: null, full_name: null, email: null }], + tags, + status: '', + search: 'hello', + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters, + tags, + search: 'hello', + }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCases({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(resp).toEqual({ ...allCases }); + }); + }); + describe('getCasesStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(casesStatusSnake); + }); + test('check url, method, signal', async () => { + await getCasesStatus(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCasesStatus(abortCtrl.signal); + expect(resp).toEqual(casesStatus); + }); + }); + describe('getCaseUserActions', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseUserActionsSnake); + }); + + test('check url, method, signal', async () => { + await getCaseUserActions(basicCase.id, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); + expect(resp).toEqual(caseUserActions); + }); + }); + describe('getReporters', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(respReporters); + }); + + test('check url, method, signal', async () => { + await getReporters(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getReporters(abortCtrl.signal); + expect(resp).toEqual(respReporters); + }); + }); + describe('getTags', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(tags); + }); + + test('check url, method, signal', async () => { + await getTags(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getTags(abortCtrl.signal); + expect(resp).toEqual(tags); + }); + }); + describe('patchCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue([basicCaseSnake]); + }); + const data = { description: 'updated description' }; + test('check url, method, signal', async () => { + await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'PATCH', + body: JSON.stringify({ + cases: [{ ...data, id: basicCase.id, version: basicCase.version }], + }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCase( + basicCase.id, + { description: 'updated description' }, + basicCase.version, + abortCtrl.signal + ); + expect(resp).toEqual({ ...[basicCase] }); + }); + }); + describe('patchCasesStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(casesSnake); + }); + const data = [ + { + status: 'closed', + id: basicCase.id, + version: basicCase.version, + }, + ]; + + test('check url, method, signal', async () => { + await patchCasesStatus(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'PATCH', + body: JSON.stringify({ cases: data }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCasesStatus(data, abortCtrl.signal); + expect(resp).toEqual({ ...cases }); + }); + }); + describe('patchComment', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + + test('check url, method, signal', async () => { + await patchComment( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { + method: 'PATCH', + body: JSON.stringify({ + comment: 'updated comment', + id: basicCase.comments[0].id, + version: basicCase.comments[0].version, + }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchComment( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + expect(resp).toEqual(basicCase); + }); + }); + describe('postCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = { + description: 'description', + tags: ['tag'], + title: 'title', + }; + + test('check url, method, signal', async () => { + await postCase(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'POST', + body: JSON.stringify(data), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCase(data, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + describe('postComment', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = { + comment: 'comment', + }; + + test('check url, method, signal', async () => { + await postComment(data, basicCase.id, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { + method: 'POST', + body: JSON.stringify(data), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postComment(data, basicCase.id, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + describe('pushCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(pushedCaseSnake); + }); + + test('check url, method, signal', async () => { + await pushCase(basicCase.id, pushSnake, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/_push`, { + method: 'POST', + body: JSON.stringify(pushSnake), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await pushCase(basicCase.id, pushSnake, abortCtrl.signal); + expect(resp).toEqual(pushedCase); + }); + }); + describe('pushToService', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(actionTypeExecutorResult); + }); + const connectorId = 'connectorId'; + test('check url, method, signal', async () => { + await pushToService(connectorId, casePushParams, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`/api/action/${connectorId}/_execute`, { + method: 'POST', + body: JSON.stringify({ params: casePushParams }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await pushToService(connectorId, casePushParams, abortCtrl.signal); + expect(resp).toEqual(serviceConnector); + }); + + test('unhappy path - serviceMessage', async () => { + const theError = 'the error'; + fetchMock.mockResolvedValue({ + ...actionTypeExecutorResult, + status: 'error', + serviceMessage: theError, + message: 'not it', + }); + await expect( + pushToService(connectorId, casePushParams, abortCtrl.signal) + ).rejects.toMatchObject({ message: theError }); + }); + + test('unhappy path - message', async () => { + const theError = 'the error'; + fetchMock.mockResolvedValue({ + ...actionTypeExecutorResult, + status: 'error', + message: theError, + }); + await expect( + pushToService(connectorId, casePushParams, abortCtrl.signal) + ).rejects.toMatchObject({ message: theError }); + }); + + test('unhappy path - no message', async () => { + const theError = i18n.ERROR_PUSH_TO_SERVICE; + fetchMock.mockResolvedValue({ + ...actionTypeExecutorResult, + status: 'error', + }); + await expect( + pushToService(connectorId, casePushParams, abortCtrl.signal) + ).rejects.toMatchObject({ message: theError }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/mock.ts new file mode 100644 index 0000000000000..0bda75e5bc9e0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/mock.ts @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; + +import { + CommentResponse, + ServiceConnectorCaseResponse, + Status, + UserAction, + UserActionField, + CaseResponse, + CasesStatusResponse, + CaseUserActionsResponse, + CasesResponse, + CasesFindResponse, +} from '../../../../../../plugins/case/common/api/cases'; +import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; + +export const basicCaseId = 'basic-case-id'; +const basicCommentId = 'basic-comment-id'; +const basicCreatedAt = '2020-02-19T23:06:33.798Z'; +const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; +const laterTime = '2020-02-28T15:02:57.995Z'; +export const elasticUser = { + fullName: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; + +export const tags: string[] = ['coke', 'pepsi']; + +export const basicComment: Comment = { + comment: 'Solve this fast!', + id: basicCommentId, + createdAt: basicCreatedAt, + createdBy: elasticUser, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', +}; + +export const basicCase: Case = { + closedAt: null, + closedBy: null, + id: basicCaseId, + comments: [basicComment], + createdAt: basicCreatedAt, + createdBy: elasticUser, + description: 'Security banana Issue', + externalService: null, + status: 'open', + tags, + title: 'Another horrible breach!!', + totalComment: 1, + updatedAt: basicUpdatedAt, + updatedBy: elasticUser, + version: 'WzQ3LDFd', +}; + +export const basicCasePost: Case = { + ...basicCase, + updatedAt: null, + updatedBy: null, +}; + +export const basicCommentPatch: Comment = { + ...basicComment, + updatedAt: basicUpdatedAt, + updatedBy: { + username: 'elastic', + }, +}; + +export const basicCaseCommentPatch = { + ...basicCase, + comments: [basicCommentPatch], +}; + +export const casesStatus: CasesStatus = { + countClosedCases: 130, + countOpenCases: 20, +}; + +const basicPush = { + connectorId: 'connector_id', + connectorName: 'connector name', + externalId: 'external_id', + externalTitle: 'external title', + externalUrl: 'basicPush.com', + pushedAt: basicUpdatedAt, + pushedBy: elasticUser, +}; + +export const pushedCase: Case = { + ...basicCase, + externalService: basicPush, +}; + +export const serviceConnector: ServiceConnectorCaseResponse = { + number: '123', + incidentId: '444', + pushedDate: basicUpdatedAt, + url: 'connector.com', + comments: [ + { + commentId: basicCommentId, + pushedDate: basicUpdatedAt, + }, + ], +}; + +const basicAction = { + actionAt: basicCreatedAt, + actionBy: elasticUser, + oldValue: null, + newValue: 'what a cool value', + caseId: basicCaseId, + commentId: null, +}; + +export const casePushParams = { + actionBy: elasticUser, + caseId: basicCaseId, + createdAt: basicCreatedAt, + createdBy: elasticUser, + incidentId: null, + title: 'what a cool value', + commentId: null, + updatedAt: basicCreatedAt, + updatedBy: elasticUser, + description: 'nice', +}; +export const actionTypeExecutorResult = { + actionId: 'string', + status: 'ok', + data: serviceConnector, +}; + +export const cases: Case[] = [ + basicCase, + { ...pushedCase, id: '1', totalComment: 0, comments: [] }, + { ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCase, id: '3', totalComment: 0, comments: [] }, + { ...basicCase, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCases: AllCases = { + cases, + page: 1, + perPage: 5, + total: 10, + ...casesStatus, +}; +export const actionLicenses: ActionLicense[] = [ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, +]; + +// Snake case for mock api responses +export const elasticUserSnake = { + full_name: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; +export const basicCommentSnake: CommentResponse = { + ...basicComment, + comment: 'Solve this fast!', + id: basicCommentId, + created_at: basicCreatedAt, + created_by: elasticUserSnake, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, +}; + +export const basicCaseSnake: CaseResponse = { + ...basicCase, + status: 'open' as Status, + closed_at: null, + closed_by: null, + comments: [basicCommentSnake], + created_at: basicCreatedAt, + created_by: elasticUserSnake, + external_service: null, + updated_at: basicUpdatedAt, + updated_by: elasticUserSnake, +}; + +export const casesStatusSnake: CasesStatusResponse = { + count_closed_cases: 130, + count_open_cases: 20, +}; + +export const pushSnake = { + connector_id: 'connector_id', + connector_name: 'connector name', + external_id: 'external_id', + external_title: 'external title', + external_url: 'basicPush.com', +}; +const basicPushSnake = { + ...pushSnake, + pushed_at: basicUpdatedAt, + pushed_by: elasticUserSnake, +}; +export const pushedCaseSnake = { + ...basicCaseSnake, + external_service: basicPushSnake, +}; + +export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; +export const respReporters = [ + { username: 'alexis', full_name: null, email: null }, + { username: 'kim', full_name: null, email: null }, + { username: 'maria', full_name: null, email: null }, + { username: 'steph', full_name: null, email: null }, +]; +export const casesSnake: CasesResponse = [ + basicCaseSnake, + { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] }, + { ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '3', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCasesSnake: CasesFindResponse = { + cases: casesSnake, + page: 1, + per_page: 5, + total: 10, + ...casesStatusSnake, +}; + +const basicActionSnake = { + action_at: basicCreatedAt, + action_by: elasticUserSnake, + old_value: null, + new_value: 'what a cool value', + case_id: basicCaseId, + comment_id: null, +}; +export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ + ...basicActionSnake, + action_id: `${af[0]}-${a}`, + action_field: af, + action: a, + comment_id: af[0] === 'comment' ? basicCommentId : null, + new_value: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const caseUserActionsSnake: CaseUserActionsResponse = [ + getUserActionSnake(['description'], 'create'), + getUserActionSnake(['comment'], 'create'), + getUserActionSnake(['description'], 'update'), +]; + +// user actions + +export const getUserAction = (af: UserActionField, a: UserAction) => ({ + ...basicAction, + actionId: `${af[0]}-${a}`, + actionField: af, + action: a, + commentId: af[0] === 'comment' ? basicCommentId : null, + newValue: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const caseUserActions: CaseUserActions[] = [ + getUserAction(['description'], 'create'), + getUserAction(['comment'], 'create'), + getUserAction(['description'], 'update'), +]; + +// components tests +export const useGetCasesMockState: UseGetCasesState = { + data: allCases, + loading: [], + selectedCases: [], + isError: false, + queryParams: DEFAULT_QUERY_PARAMS, + filterOptions: DEFAULT_FILTER_OPTIONS, +}; + +export const basicCaseClosed: Case = { + ...basicCase, + closedAt: '2020-02-25T23:06:33.798Z', + closedBy: elasticUser, + status: 'closed', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index d2a58e9eeeff4..e552f22b55fa4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -31,7 +31,7 @@ export interface CaseUserActions { export interface CaseExternalService { pushedAt: string; - pushedBy: string; + pushedBy: ElasticUser; connectorId: string; connectorName: string; externalId: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx new file mode 100644 index 0000000000000..329fda10424a8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useUpdateCases, UseUpdateCases } from './use_bulk_update_case'; +import { basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useUpdateCases', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCases() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + isUpdated: false, + updateBulkStatus: result.current.updateBulkStatus, + dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, + }); + }); + }); + + it('calls patchCase with correct arguments', async () => { + const spyOnPatchCases = jest.spyOn(api, 'patchCasesStatus'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCases() + ); + await waitForNextUpdate(); + + result.current.updateBulkStatus([basicCase], 'closed'); + await waitForNextUpdate(); + expect(spyOnPatchCases).toBeCalledWith( + [ + { + status: 'closed', + id: basicCase.id, + version: basicCase.version, + }, + ], + abortCtrl.signal + ); + }); + }); + + it('patch cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCases() + ); + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isUpdated: true, + isLoading: false, + isError: false, + updateBulkStatus: result.current.updateBulkStatus, + dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCases() + ); + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('dispatchResetIsUpdated resets is updated', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCases() + ); + + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + await waitForNextUpdate(); + expect(result.current.isUpdated).toBeTruthy(); + result.current.dispatchResetIsUpdated(); + expect(result.current.isUpdated).toBeFalsy(); + }); + }); + + it('unhappy path', async () => { + const spyOnPatchCases = jest.spyOn(api, 'patchCasesStatus'); + spyOnPatchCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCases() + ); + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + + expect(result.current).toEqual({ + isUpdated: false, + isLoading: false, + isError: true, + updateBulkStatus: result.current.updateBulkStatus, + dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx index 7d040c49f1971..d0cc4d99f8f9f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx @@ -51,12 +51,12 @@ const dataFetchReducer = (state: UpdateState, action: Action): UpdateState => { return state; } }; -interface UseUpdateCase extends UpdateState { +export interface UseUpdateCases extends UpdateState { updateBulkStatus: (cases: Case[], status: string) => void; dispatchResetIsUpdated: () => void; } -export const useUpdateCases = (): UseUpdateCase => { +export const useUpdateCases = (): UseUpdateCases => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx new file mode 100644 index 0000000000000..45ba392f3b5b4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useDeleteCases, UseDeleteCase } from './use_delete_cases'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useDeleteCases', () => { + const abortCtrl = new AbortController(); + const deleteObj = [{ id: '1' }, { id: '2' }, { id: '3' }]; + const deleteArr = ['1', '2', '3']; + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useDeleteCases() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: false, + isDeleted: false, + dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, + handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, + handleToggleModal: result.current.handleToggleModal, + }); + }); + }); + + it('calls deleteCases with correct arguments', async () => { + const spyOnDeleteCases = jest.spyOn(api, 'deleteCases'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useDeleteCases() + ); + await waitForNextUpdate(); + + result.current.handleOnDeleteConfirm(deleteObj); + await waitForNextUpdate(); + expect(spyOnDeleteCases).toBeCalledWith(deleteArr, abortCtrl.signal); + }); + }); + + it('deletes cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: false, + isDeleted: true, + dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, + handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, + handleToggleModal: result.current.handleToggleModal, + }); + }); + }); + + it('resets is deleting', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + await waitForNextUpdate(); + expect(result.current.isDeleted).toBeTruthy(); + result.current.handleToggleModal(); + result.current.dispatchResetIsDeleted(); + expect(result.current.isDeleted).toBeFalsy(); + }); + }); + + it('set isLoading to true when deleting cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnDeleteCases = jest.spyOn(api, 'deleteCases'); + spyOnDeleteCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + + expect(result.current).toEqual({ + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: true, + isDeleted: false, + dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, + handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, + handleToggleModal: result.current.handleToggleModal, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx index 07e3786758aeb..3c49be551c064 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -59,9 +59,9 @@ const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { } }; -interface UseDeleteCase extends DeleteState { +export interface UseDeleteCase extends DeleteState { dispatchResetIsDeleted: () => void; - handleOnDeleteConfirm: (caseIds: DeleteCase[]) => void; + handleOnDeleteConfirm: (cases: DeleteCase[]) => void; handleToggleModal: () => void; } @@ -117,8 +117,8 @@ export const useDeleteCases = (): UseDeleteCase => { }, [state.isDisplayConfirmDeleteModal]); const handleOnDeleteConfirm = useCallback( - caseIds => { - dispatchDeleteCases(caseIds); + (cases: DeleteCase[]) => { + dispatchDeleteCases(cases); dispatchToggleDeleteModal(); }, [state.isDisplayConfirmDeleteModal] diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx new file mode 100644 index 0000000000000..23c9ff5e49586 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.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 { renderHook, act } from '@testing-library/react-hooks'; +import { initialData, useGetActionLicense, ActionLicenseState } from './use_get_action_license'; +import { actionLicenses } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetActionLicense', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + expect(result.current).toEqual(initialData); + }); + }); + + it('calls getActionLicense with correct arguments', async () => { + const spyOnGetActionLicense = jest.spyOn(api, 'getActionLicense'); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetActionLicense).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('gets action license', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + actionLicense: actionLicenses[0], + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnGetActionLicense = jest.spyOn(api, 'getActionLicense'); + spyOnGetActionLicense.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + actionLicense: null, + isLoading: false, + isError: true, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx index 12f92b2db039b..0d28a1b20c61f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx @@ -11,13 +11,13 @@ import { getActionLicense } from './api'; import * as i18n from './translations'; import { ActionLicense } from './types'; -interface ActionLicenseState { +export interface ActionLicenseState { actionLicense: ActionLicense | null; isLoading: boolean; isError: boolean; } -const initialData: ActionLicenseState = { +export const initialData: ActionLicenseState = { actionLicense: null, isLoading: true, isError: false, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx new file mode 100644 index 0000000000000..10649da548d43 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { renderHook, act } from '@testing-library/react-hooks'; +import { initialData, useGetCase, UseGetCase } from './use_get_case'; +import { basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCase', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: initialData, + isLoading: true, + isError: false, + fetchCase: result.current.fetchCase, + updateCase: result.current.updateCase, + }); + }); + }); + + it('calls getCase with correct arguments', async () => { + const spyOnGetCase = jest.spyOn(api, 'getCase'); + await act(async () => { + const { waitForNextUpdate } = renderHook(() => useGetCase(basicCase.id)); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); + }); + }); + + it('fetch case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: basicCase, + isLoading: false, + isError: false, + fetchCase: result.current.fetchCase, + updateCase: result.current.updateCase, + }); + }); + }); + + it('refetch case', async () => { + const spyOnGetCase = jest.spyOn(api, 'getCase'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchCase(); + expect(spyOnGetCase).toHaveBeenCalledTimes(2); + }); + }); + + it('set isLoading to true when refetching case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchCase(); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnGetCase = jest.spyOn(api, 'getCase'); + spyOnGetCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + data: initialData, + isLoading: false, + isError: true, + fetchCase: result.current.fetchCase, + updateCase: result.current.updateCase, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index 835fb7153dc95..b2e3b6d0cacf6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -53,7 +53,7 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { return state; } }; -const initialData: Case = { +export const initialData: Case = { id: '', closedAt: null, closedBy: null, @@ -73,7 +73,7 @@ const initialData: Case = { version: '', }; -interface UseGetCase extends CaseState { +export interface UseGetCase extends CaseState { fetchCase: () => void; updateCase: (newCase: Case) => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx new file mode 100644 index 0000000000000..cdd40b84f8724 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { renderHook, act } from '@testing-library/react-hooks'; +import { + initialData, + useGetCaseUserActions, + UseGetCaseUserActions, +} from './use_get_case_user_actions'; +import { basicCaseId, caseUserActions, elasticUser } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCaseUserActions', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialData, + fetchCaseUserActions: result.current.fetchCaseUserActions, + }); + }); + }); + + it('calls getCaseUserActions with correct arguments', async () => { + const spyOnPostCase = jest.spyOn(api, 'getCaseUserActions'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + + result.current.fetchCaseUserActions(basicCaseId); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(basicCaseId, abortCtrl.signal); + }); + }); + + it('retuns proper state on getCaseUserActions', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + result.current.fetchCaseUserActions(basicCaseId); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialData, + caseUserActions: caseUserActions.slice(1), + fetchCaseUserActions: result.current.fetchCaseUserActions, + hasDataToPush: true, + isError: false, + isLoading: false, + participants: [elasticUser], + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + result.current.fetchCaseUserActions(basicCaseId); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPostCase = jest.spyOn(api, 'getCaseUserActions'); + spyOnPostCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + result.current.fetchCaseUserActions(basicCaseId); + + expect(result.current).toEqual({ + ...initialData, + isLoading: false, + isError: true, + fetchCaseUserActions: result.current.fetchCaseUserActions, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx index 4c278bc038134..6d9874a655e97 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx @@ -22,7 +22,7 @@ interface CaseUserActionsState { lastIndexPushToService: number; } -const initialData: CaseUserActionsState = { +export const initialData: CaseUserActionsState = { caseUserActions: [], firstIndexPushToService: -1, lastIndexPushToService: -1, @@ -32,7 +32,7 @@ const initialData: CaseUserActionsState = { participants: [], }; -interface UseGetCaseUserActions extends CaseUserActionsState { +export interface UseGetCaseUserActions extends CaseUserActionsState { fetchCaseUserActions: (caseId: string) => void; } @@ -80,6 +80,7 @@ export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions => const participants = !isEmpty(response) ? uniqBy('actionBy.username', response).map(cau => cau.actionBy) : []; + const caseUserActions = !isEmpty(response) ? response.slice(1) : []; setCaseUserActionsState({ caseUserActions, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx new file mode 100644 index 0000000000000..4e274e074b036 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { renderHook, act } from '@testing-library/react-hooks'; +import { + DEFAULT_FILTER_OPTIONS, + DEFAULT_QUERY_PARAMS, + initialData, + useGetCases, + UseGetCases, +} from './use_get_cases'; +import { UpdateKey } from './use_update_case'; +import { allCases, basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCases', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: initialData, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: false, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + + it('calls getCases with correct arguments', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + await act(async () => { + const { waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetCases).toBeCalledWith({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + }); + + it('fetch cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: allCases, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: false, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + it('dispatch update case property', async () => { + const spyOnPatchCase = jest.spyOn(api, 'patchCase'); + await act(async () => { + const updateCase = { + updateKey: 'description' as UpdateKey, + updateValue: 'description update', + caseId: basicCase.id, + refetchCasesStatus: jest.fn(), + version: '99999', + }; + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.dispatchUpdateCaseProperty(updateCase); + expect(result.current.loading).toEqual(['caseUpdate']); + expect(spyOnPatchCase).toBeCalledWith( + basicCase.id, + { [updateCase.updateKey]: updateCase.updateValue }, + updateCase.version, + abortCtrl.signal + ); + }); + }); + + it('refetch cases', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCases(); + expect(spyOnGetCases).toHaveBeenCalledTimes(2); + }); + }); + + it('set isLoading to true when refetching case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCases(); + + expect(result.current.loading).toEqual(['cases']); + }); + }); + + it('unhappy path', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + spyOnGetCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + data: initialData, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: true, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + it('set filters', async () => { + await act(async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + const newFilters = { + search: 'new', + tags: ['new'], + status: 'closed', + }; + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setFilters(newFilters); + await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, ...newFilters }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + }); + it('set query params', async () => { + await act(async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + const newQueryParams = { + page: 2, + }; + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setQueryParams(newQueryParams); + await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: { ...DEFAULT_QUERY_PARAMS, ...newQueryParams }, + signal: abortCtrl.signal, + }); + }); + }); + it('set selected cases', async () => { + await act(async () => { + const selectedCases = [basicCase]; + const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setSelectedCases(selectedCases); + expect(result.current.selectedCases).toEqual(selectedCases); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 1cbce5af6304b..465b50dbdc1bc 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -105,7 +105,7 @@ export const DEFAULT_QUERY_PARAMS: QueryParams = { sortOrder: 'desc', }; -const initialData: AllCases = { +export const initialData: AllCases = { cases: [], countClosedCases: null, countOpenCases: null, @@ -113,7 +113,7 @@ const initialData: AllCases = { perPage: 0, total: 0, }; -interface UseGetCases extends UseGetCasesState { +export interface UseGetCases extends UseGetCasesState { dispatchUpdateCaseProperty: ({ updateKey, updateValue, @@ -121,7 +121,7 @@ interface UseGetCases extends UseGetCasesState { version, refetchCasesStatus, }: UpdateCase) => void; - refetchCases: (filters: FilterOptions, queryParams: QueryParams) => void; + refetchCases: () => void; setFilters: (filters: Partial) => void; setQueryParams: (queryParams: Partial) => void; setSelectedCases: (mySelectedCases: Case[]) => void; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx new file mode 100644 index 0000000000000..bfbcbd2525e3b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.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 { renderHook, act } from '@testing-library/react-hooks'; +import { useGetCasesStatus, UseGetCasesStatus } from './use_get_cases_status'; +import { casesStatus } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCasesStatus', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + countClosedCases: null, + countOpenCases: null, + isLoading: true, + isError: false, + fetchCasesStatus: result.current.fetchCasesStatus, + }); + }); + }); + + it('calls getCasesStatus api', async () => { + const spyOnGetCasesStatus = jest.spyOn(api, 'getCasesStatus'); + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetCasesStatus).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('fetch reporters', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + countClosedCases: casesStatus.countClosedCases, + countOpenCases: casesStatus.countOpenCases, + isLoading: false, + isError: false, + fetchCasesStatus: result.current.fetchCasesStatus, + }); + }); + }); + + it('unhappy path', async () => { + const spyOnGetCasesStatus = jest.spyOn(api, 'getCasesStatus'); + spyOnGetCasesStatus.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + countClosedCases: 0, + countOpenCases: 0, + isLoading: false, + isError: true, + fetchCasesStatus: result.current.fetchCasesStatus, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx index 7f56d27ef160e..0788464602357 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx @@ -23,7 +23,7 @@ const initialData: CasesStatusState = { isError: false, }; -interface UseGetCasesStatus extends CasesStatusState { +export interface UseGetCasesStatus extends CasesStatusState { fetchCasesStatus: () => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx new file mode 100644 index 0000000000000..27b963eb6cb54 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.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 { renderHook, act } from '@testing-library/react-hooks'; +import { useGetReporters, UseGetReporters } from './use_get_reporters'; +import { reporters, respReporters } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetReporters', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetReporters() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + reporters: [], + respReporters: [], + isLoading: true, + isError: false, + fetchReporters: result.current.fetchReporters, + }); + }); + }); + + it('calls getReporters api', async () => { + const spyOnGetReporters = jest.spyOn(api, 'getReporters'); + await act(async () => { + const { waitForNextUpdate } = renderHook(() => useGetReporters()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetReporters).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('fetch reporters', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetReporters() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + reporters, + respReporters, + isLoading: false, + isError: false, + fetchReporters: result.current.fetchReporters, + }); + }); + }); + + it('refetch reporters', async () => { + const spyOnGetReporters = jest.spyOn(api, 'getReporters'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetReporters() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchReporters(); + expect(spyOnGetReporters).toHaveBeenCalledTimes(2); + }); + }); + + it('unhappy path', async () => { + const spyOnGetReporters = jest.spyOn(api, 'getReporters'); + spyOnGetReporters.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetReporters() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + reporters: [], + respReporters: [], + isLoading: false, + isError: true, + fetchReporters: result.current.fetchReporters, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx index 2478172a3394b..2fc9b8294c8e0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx @@ -26,7 +26,7 @@ const initialData: ReportersState = { isError: false, }; -interface UseGetReporters extends ReportersState { +export interface UseGetReporters extends ReportersState { fetchReporters: () => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx new file mode 100644 index 0000000000000..2d70c4390e4dd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.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 { renderHook, act } from '@testing-library/react-hooks'; +import { useGetTags, UseGetTags } from './use_get_tags'; +import { tags } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetTags', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + tags: [], + isLoading: true, + isError: false, + fetchTags: result.current.fetchTags, + }); + }); + }); + + it('calls getTags api', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + await act(async () => { + const { waitForNextUpdate } = renderHook(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('fetch tags', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + tags, + isLoading: false, + isError: false, + fetchTags: result.current.fetchTags, + }); + }); + }); + + it('refetch tags', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchTags(); + expect(spyOnGetTags).toHaveBeenCalledTimes(2); + }); + }); + + it('unhappy path', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + spyOnGetTags.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + tags: [], + isLoading: false, + isError: true, + fetchTags: result.current.fetchTags, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx index b41d5aab5c07a..99bb65fa160f7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -10,7 +10,7 @@ import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getTags } from './api'; import * as i18n from './translations'; -interface TagsState { +export interface TagsState { tags: string[]; isLoading: boolean; isError: boolean; @@ -20,6 +20,10 @@ type Action = | { type: 'FETCH_SUCCESS'; payload: string[] } | { type: 'FETCH_FAILURE' }; +export interface UseGetTags extends TagsState { + fetchTags: () => void; +} + const dataFetchReducer = (state: TagsState, action: Action): TagsState => { switch (action.type) { case 'FETCH_INIT': @@ -47,15 +51,15 @@ const dataFetchReducer = (state: TagsState, action: Action): TagsState => { }; const initialData: string[] = []; -export const useGetTags = (): TagsState => { +export const useGetTags = (): UseGetTags => { const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: false, + isLoading: true, isError: false, tags: initialData, }); const [, dispatchToaster] = useStateToaster(); - useEffect(() => { + const callFetch = () => { let didCancel = false; const abortCtrl = new AbortController(); @@ -82,6 +86,9 @@ export const useGetTags = (): TagsState => { abortCtrl.abort(); didCancel = true; }; + }; + useEffect(() => { + callFetch(); }, []); - return state; + return { ...state, fetchTags: callFetch }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx new file mode 100644 index 0000000000000..8b105fe041d27 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.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 { renderHook, act } from '@testing-library/react-hooks'; +import { usePostCase, UsePostCase } from './use_post_case'; +import { basicCasePost } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePostCase', () => { + const abortCtrl = new AbortController(); + const samplePost = { + description: 'description', + tags: ['tags'], + title: 'title', + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => usePostCase()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + caseData: null, + postCase: result.current.postCase, + }); + }); + }); + + it('calls postCase with correct arguments', async () => { + const spyOnPostCase = jest.spyOn(api, 'postCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => usePostCase()); + await waitForNextUpdate(); + + result.current.postCase(samplePost); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(samplePost, abortCtrl.signal); + }); + }); + + it('post case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + await waitForNextUpdate(); + expect(result.current).toEqual({ + caseData: basicCasePost, + isLoading: false, + isError: false, + postCase: result.current.postCase, + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPostCase = jest.spyOn(api, 'postCase'); + spyOnPostCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + + expect(result.current).toEqual({ + caseData: null, + isLoading: false, + isError: true, + postCase: result.current.postCase, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 0e01364721dc5..aeb50fc098eee 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -48,7 +48,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => } }; -interface UsePostCase extends NewCaseState { +export interface UsePostCase extends NewCaseState { postCase: (data: CasePostRequest) => void; } export const usePostCase = (): UsePostCase => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx new file mode 100644 index 0000000000000..d7d9cf9c557c9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { renderHook, act } from '@testing-library/react-hooks'; +import { usePostComment, UsePostComment } from './use_post_comment'; +import { basicCaseId } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePostComment', () => { + const abortCtrl = new AbortController(); + const samplePost = { + comment: 'a comment', + }; + const updateCaseCallback = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + postComment: result.current.postComment, + }); + }); + }); + + it('calls postComment with correct arguments', async () => { + const spyOnPostCase = jest.spyOn(api, 'postComment'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + + result.current.postComment(samplePost, updateCaseCallback); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal); + }); + }); + + it('post case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + result.current.postComment(samplePost, updateCaseCallback); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + postComment: result.current.postComment, + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + result.current.postComment(samplePost, updateCaseCallback); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPostCase = jest.spyOn(api, 'postComment'); + spyOnPostCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + result.current.postComment(samplePost, updateCaseCallback); + + expect(result.current).toEqual({ + isLoading: false, + isError: true, + postComment: result.current.postComment, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx index 207b05814717f..c6d34b5449977 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -41,7 +41,7 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta } }; -interface UsePostComment extends NewCommentState { +export interface UsePostComment extends NewCommentState { postComment: (data: CommentRequest, updateCase: (newCase: Case) => void) => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx new file mode 100644 index 0000000000000..b07a346a8da46 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { renderHook, act } from '@testing-library/react-hooks'; +import { + formatServiceRequestData, + usePostPushToService, + UsePostPushToService, +} from './use_post_push_to_service'; +import { basicCase, pushedCase, serviceConnector } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePostPushToService', () => { + const abortCtrl = new AbortController(); + const updateCase = jest.fn(); + const samplePush = { + caseId: pushedCase.id, + connectorName: 'sample', + connectorId: '22', + updateCase, + }; + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostPushToService() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + serviceData: null, + pushedCaseData: null, + isLoading: false, + isError: false, + postPushToService: result.current.postPushToService, + }); + }); + }); + + it('calls pushCase with correct arguments', async () => { + const spyOnPushCase = jest.spyOn(api, 'pushCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + expect(spyOnPushCase).toBeCalledWith( + samplePush.caseId, + { + connector_id: samplePush.connectorId, + connector_name: samplePush.connectorName, + external_id: serviceConnector.incidentId, + external_title: serviceConnector.number, + external_url: serviceConnector.url, + }, + abortCtrl.signal + ); + }); + }); + + it('calls pushToService with correct arguments', async () => { + const spyOnPushToService = jest.spyOn(api, 'pushToService'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + expect(spyOnPushToService).toBeCalledWith( + samplePush.connectorId, + formatServiceRequestData(basicCase), + abortCtrl.signal + ); + }); + }); + + it('post push to service', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + expect(result.current).toEqual({ + serviceData: serviceConnector, + pushedCaseData: pushedCase, + isLoading: false, + isError: false, + postPushToService: result.current.postPushToService, + }); + }); + }); + + it('set isLoading to true when deleting cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPushToService = jest.spyOn(api, 'pushToService'); + spyOnPushToService.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + serviceData: null, + pushedCaseData: null, + isLoading: false, + isError: true, + postPushToService: result.current.postPushToService, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx index d9a32f26f7fe7..89e7e18cf0688 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -68,7 +68,7 @@ interface PushToServiceRequest { updateCase: (newCase: Case) => void; } -interface UsePostPushToService extends PushToServiceState { +export interface UsePostPushToService extends PushToServiceState { postPushToService: ({ caseId, connectorId, updateCase }: PushToServiceRequest) => void; } @@ -131,7 +131,7 @@ export const usePostPushToService = (): UsePostPushToService => { return { ...state, postPushToService }; }; -const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { +export const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { const { id: caseId, createdAt, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx new file mode 100644 index 0000000000000..86cfc3459c595 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useUpdateCase, UseUpdateCase, UpdateKey } from './use_update_case'; +import { basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useUpdateCase', () => { + const abortCtrl = new AbortController(); + const fetchCaseUserActions = jest.fn(); + const updateCase = jest.fn(); + const updateKey: UpdateKey = 'description'; + const sampleUpdate = { + fetchCaseUserActions, + updateKey, + updateValue: 'updated description', + updateCase, + version: basicCase.version, + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + updateKey: null, + updateCaseProperty: result.current.updateCaseProperty, + }); + }); + }); + + it('calls patchCase with correct arguments', async () => { + const spyOnPatchCase = jest.spyOn(api, 'patchCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + + result.current.updateCaseProperty(sampleUpdate); + await waitForNextUpdate(); + expect(spyOnPatchCase).toBeCalledWith( + basicCase.id, + { description: 'updated description' }, + basicCase.version, + abortCtrl.signal + ); + }); + }); + + it('patch case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + result.current.updateCaseProperty(sampleUpdate); + await waitForNextUpdate(); + expect(result.current).toEqual({ + updateKey: null, + isLoading: false, + isError: false, + updateCaseProperty: result.current.updateCaseProperty, + }); + expect(fetchCaseUserActions).toBeCalledWith(basicCase.id); + expect(updateCase).toBeCalledWith(basicCase); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + result.current.updateCaseProperty(sampleUpdate); + + expect(result.current.isLoading).toBe(true); + expect(result.current.updateKey).toBe(updateKey); + }); + }); + + it('unhappy path', async () => { + const spyOnPatchCase = jest.spyOn(api, 'patchCase'); + spyOnPatchCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + result.current.updateCaseProperty(sampleUpdate); + + expect(result.current).toEqual({ + updateKey: null, + isLoading: false, + isError: true, + updateCaseProperty: result.current.updateCaseProperty, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 4973deef4d91a..7ebbbba076c12 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -12,7 +12,7 @@ import { patchCase } from './api'; import * as i18n from './translations'; import { Case } from './types'; -type UpdateKey = keyof Pick; +export type UpdateKey = keyof Pick; interface NewCaseState { isLoading: boolean; @@ -62,7 +62,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => } }; -interface UseUpdateCase extends NewCaseState { +export interface UseUpdateCase extends NewCaseState { updateCaseProperty: (updates: UpdateByKey) => void; } export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx new file mode 100644 index 0000000000000..5772ff4246866 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useUpdateComment, UseUpdateComment } from './use_update_comment'; +import { basicCase, basicCaseCommentPatch } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useUpdateComment', () => { + const abortCtrl = new AbortController(); + const fetchUserActions = jest.fn(); + const updateCase = jest.fn(); + const sampleUpdate = { + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + fetchUserActions, + updateCase, + version: basicCase.comments[0].version, + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateComment() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoadingIds: [], + isError: false, + patchComment: result.current.patchComment, + }); + }); + }); + + it('calls patchComment with correct arguments', async () => { + const spyOnPatchComment = jest.spyOn(api, 'patchComment'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateComment() + ); + await waitForNextUpdate(); + + result.current.patchComment(sampleUpdate); + await waitForNextUpdate(); + expect(spyOnPatchComment).toBeCalledWith( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + }); + }); + + it('patch comment', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateComment() + ); + await waitForNextUpdate(); + result.current.patchComment(sampleUpdate); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoadingIds: [], + isError: false, + patchComment: result.current.patchComment, + }); + expect(fetchUserActions).toBeCalled(); + expect(updateCase).toBeCalledWith(basicCaseCommentPatch); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateComment() + ); + await waitForNextUpdate(); + result.current.patchComment(sampleUpdate); + + expect(result.current.isLoadingIds).toEqual([basicCase.comments[0].id]); + }); + }); + + it('unhappy path', async () => { + const spyOnPatchComment = jest.spyOn(api, 'patchComment'); + spyOnPatchComment.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateComment() + ); + await waitForNextUpdate(); + result.current.patchComment(sampleUpdate); + + expect(result.current).toEqual({ + isLoadingIds: [], + isError: true, + patchComment: result.current.patchComment, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx index faf9649a705c5..ffc5cffee7a55 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -60,7 +60,7 @@ interface UpdateComment { version: string; } -interface UseUpdateComment extends CommentUpdateState { +export interface UseUpdateComment extends CommentUpdateState { patchComment: ({ caseId, commentId, commentUpdate, fetchUserActions }: UpdateComment) => void; } diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx index c7692755c1330..952f7f51b63f2 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx @@ -20,6 +20,7 @@ import { ThemeProvider } from 'styled-components'; import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; import { createKibanaContextProviderMock } from './kibana_react'; +import { FieldHook, useForm } from '../shared_imports'; jest.mock('ui/new_platform'); @@ -91,3 +92,29 @@ const TestProviderWithoutDragAndDropComponent: React.FC = ({ ); export const TestProviderWithoutDragAndDrop = React.memo(TestProviderWithoutDragAndDropComponent); + +export const useFormFieldMock = (options?: Partial): FieldHook => { + const { form } = useForm(); + + return { + path: 'path', + type: 'type', + value: [], + isPristine: false, + isValidating: false, + isValidated: false, + isChangingValue: false, + form, + errors: [], + isValid: true, + getErrorsMessages: jest.fn(), + onChange: jest.fn(), + setValue: jest.fn(), + setErrors: jest.fn(), + clearErrors: jest.fn(), + validate: jest.fn(), + reset: jest.fn(), + __serializeOutput: jest.fn(), + ...options, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index aefb0a93366b8..2b613f6692df1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -26,7 +26,6 @@ export const CasesPage = React.memo(() => { message={savedObjectReadOnly.description} /> )} - diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/case_data.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/case_data.tsx deleted file mode 100644 index 64c6276fc1be2..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/case_data.tsx +++ /dev/null @@ -1,226 +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 { CaseProps } from '../case_view'; -import { Case, Comment, SortFieldCase } from '../../../../containers/case/types'; -import { UseGetCasesState } from '../../../../containers/case/use_get_cases'; -import { UserAction, UserActionField } from '../../../../../../../../plugins/case/common/api/cases'; - -const updateCase = jest.fn(); -const fetchCase = jest.fn(); - -const basicCaseId = 'basic-case-id'; -const basicCommentId = 'basic-comment-id'; -const basicCreatedAt = '2020-02-20T23:06:33.798Z'; -const elasticUser = { - fullName: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', -}; - -export const basicComment: Comment = { - comment: 'Solve this fast!', - id: basicCommentId, - createdAt: basicCreatedAt, - createdBy: elasticUser, - pushedAt: null, - pushedBy: null, - updatedAt: '2020-02-20T23:06:33.798Z', - updatedBy: { - username: 'elastic', - }, - version: 'WzQ3LDFc', -}; - -export const basicCase: Case = { - closedAt: null, - closedBy: null, - id: basicCaseId, - comments: [basicComment], - createdAt: '2020-02-13T19:44:23.627Z', - createdBy: elasticUser, - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags: ['defacement'], - title: 'Another horrible breach!!', - totalComment: 1, - updatedAt: '2020-02-19T15:02:57.995Z', - updatedBy: { - username: 'elastic', - }, - version: 'WzQ3LDFd', -}; - -export const caseProps: CaseProps = { - caseId: basicCaseId, - userCanCrud: true, - caseData: basicCase, - fetchCase, - updateCase, -}; - -export const caseClosedProps: CaseProps = { - ...caseProps, - caseData: { - ...caseProps.caseData, - closedAt: '2020-02-20T23:06:33.798Z', - closedBy: { - username: 'elastic', - }, - status: 'closed', - }, -}; - -export const basicCaseClosed: Case = { - ...caseClosedProps.caseData, -}; - -const basicAction = { - actionAt: basicCreatedAt, - actionBy: elasticUser, - oldValue: null, - newValue: 'what a cool value', - caseId: basicCaseId, - commentId: null, -}; -export const caseUserActions = [ - { - ...basicAction, - actionBy: elasticUser, - actionField: ['comment'], - action: 'create', - actionId: 'tt', - }, -]; - -export const useGetCasesMockState: UseGetCasesState = { - data: { - countClosedCases: 0, - countOpenCases: 5, - cases: [ - basicCase, - { - closedAt: null, - closedBy: null, - id: '362a5c10-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:13.328Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: { - pushedAt: '2020-02-13T19:45:01.901Z', - pushedBy: 'elastic', - connectorId: 'string', - connectorName: 'string', - externalId: 'string', - externalTitle: 'string', - externalUrl: 'string', - }, - status: 'open', - tags: ['phishing'], - title: 'Bad email', - totalComment: 0, - updatedAt: '2020-02-13T15:45:01.901Z', - updatedBy: { username: 'elastic' }, - version: 'WzQ3LDFd', - }, - { - closedAt: null, - closedBy: null, - id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:11.328Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: { - pushedAt: '2020-02-13T19:45:01.901Z', - pushedBy: 'elastic', - connectorId: 'string', - connectorName: 'string', - externalId: 'string', - externalTitle: 'string', - externalUrl: 'string', - }, - status: 'open', - tags: ['phishing'], - title: 'Bad email', - totalComment: 0, - updatedAt: '2020-02-14T19:45:01.901Z', - updatedBy: { username: 'elastic' }, - version: 'WzQ3LDFd', - }, - { - closedAt: '2020-02-13T19:44:13.328Z', - closedBy: { username: 'elastic' }, - id: '31890e90-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:05.563Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: null, - status: 'closed', - tags: ['phishing'], - title: 'Uh oh', - totalComment: 0, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFd', - }, - { - closedAt: null, - closedBy: null, - id: '2f5b3210-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:01.901Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags: ['phishing'], - title: 'Uh oh', - totalComment: 0, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFd', - }, - ], - page: 1, - perPage: 5, - total: 10, - }, - loading: [], - selectedCases: [], - isError: false, - queryParams: { - page: 1, - perPage: 5, - sortField: SortFieldCase.createdAt, - sortOrder: 'desc', - }, - filterOptions: { search: '', reporters: [], tags: [], status: 'open' }, -}; - -const basicPush = { - connector_id: 'connector_id', - connector_name: 'connector name', - external_id: 'external_id', - external_title: 'external title', - external_url: 'basicPush.com', - pushed_at: basicCreatedAt, - pushed_by: elasticUser, -}; -export const getUserAction = (af: UserActionField, a: UserAction) => ({ - ...basicAction, - actionId: `${af[0]}-${a}`, - actionField: af, - action: a, - commentId: af[0] === 'comment' ? basicCommentId : null, - newValue: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPush) - : basicAction.newValue, -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx index e008b94ab9e16..31c795c05edd5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import { ServiceNowColumn } from './columns'; -import { useGetCasesMockState } from '../__mock__/case_data'; +import { useGetCasesMockState } from '../../../../containers/case/mock'; describe('ServiceNowColumn ', () => { it('Not pushed render', () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index f65736e7cd109..58d0c1b0faaf3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import moment from 'moment-timezone'; import { AllCases } from './'; import { TestProviders } from '../../../../mock'; -import { useGetCasesMockState } from '../__mock__/case_data'; +import { useGetCasesMockState } from '../../../../containers/case/mock'; import * as i18n from './translations'; import { getEmptyTagValue } from '../../../../components/empty_value'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index b0ff3dbada6c9..a6eca717a82a3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -129,13 +129,25 @@ export const AllCases = React.memo(({ userCanCrud }) => { id: '', }); const [deleteBulk, setDeleteBulk] = useState([]); - - const refreshCases = useCallback(() => { - refetchCases(filterOptions, queryParams); - fetchCasesStatus(); - setSelectedCases([]); - setDeleteBulk([]); - }, [filterOptions, queryParams]); + const filterRefetch = useRef<() => void>(); + const setFilterRefetch = useCallback( + (refetchFilter: () => void) => { + filterRefetch.current = refetchFilter; + }, + [filterRefetch.current] + ); + const refreshCases = useCallback( + (dataRefresh = true) => { + if (dataRefresh) refetchCases(); + fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); + if (filterRefetch.current != null) { + filterRefetch.current(); + } + }, + [filterOptions, queryParams, filterRefetch.current] + ); useEffect(() => { if (isDeleted) { @@ -247,6 +259,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { }; } setQueryParams(newQueryParams); + refreshCases(false); }, [queryParams] ); @@ -259,6 +272,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { setQueryParams({ sortField: SortFieldCase.createdAt }); } setFilters(newFilterOptions); + refreshCases(false); }, [filterOptions, queryParams] ); @@ -347,6 +361,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { tags: filterOptions.tags, status: filterOptions.status, }} + setFilterRefetch={setFilterRefetch} /> {isCasesLoading && isDataEmpty ? (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx index 615d052347203..21dcc9732440d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx @@ -19,17 +19,20 @@ jest.mock('../../../../containers/case/use_get_tags'); const onFilterChanged = jest.fn(); const fetchReporters = jest.fn(); +const fetchTags = jest.fn(); +const setFilterRefetch = jest.fn(); const props = { countClosedCases: 1234, countOpenCases: 99, onFilterChanged, initial: DEFAULT_FILTER_OPTIONS, + setFilterRefetch, }; describe('CasesTableFilters ', () => { beforeEach(() => { jest.resetAllMocks(); - (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'] }); + (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags }); (useGetReporters as jest.Mock).mockReturnValue({ reporters: ['casetester'], respReporters: [{ username: 'casetester' }], @@ -57,7 +60,7 @@ describe('CasesTableFilters ', () => { .text() ).toEqual('Closed cases (1234)'); }); - it('should call onFilterChange when tags change', () => { + it('should call onFilterChange when selected tags change', () => { const wrapper = mount( @@ -74,7 +77,7 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] }); }); - it('should call onFilterChange when reporters change', () => { + it('should call onFilterChange when selected reporters change', () => { const wrapper = mount( @@ -118,4 +121,45 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toBeCalledWith({ status: 'closed' }); }); + it('should call on load setFilterRefetch', () => { + mount( + + + + ); + expect(setFilterRefetch).toHaveBeenCalled(); + }); + it('should remove tag from selected tags when tag no longer exists', () => { + const ourProps = { + ...props, + initial: { + ...DEFAULT_FILTER_OPTIONS, + tags: ['pepsi', 'rc'], + }, + }; + mount( + + + + ); + expect(onFilterChanged).toHaveBeenCalledWith({ tags: ['pepsi'] }); + }); + it('should remove reporter from selected reporters when reporter no longer exists', () => { + const ourProps = { + ...props, + initial: { + ...DEFAULT_FILTER_OPTIONS, + reporters: [ + { username: 'casetester', full_name: null, email: null }, + { username: 'batman', full_name: null, email: null }, + ], + }, + }; + mount( + + + + ); + expect(onFilterChanged).toHaveBeenCalledWith({ reporters: [{ username: 'casetester' }] }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index da477a56c0a22..901fb133753e8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { isEqual } from 'lodash/fp'; import { EuiFieldSearch, @@ -25,6 +25,7 @@ interface CasesTableFiltersProps { countOpenCases: number | null; onFilterChanged: (filterOptions: Partial) => void; initial: FilterOptions; + setFilterRefetch: (val: () => void) => void; } /** @@ -41,6 +42,7 @@ const CasesTableFiltersComponent = ({ countOpenCases, onFilterChanged, initial = defaultInitial, + setFilterRefetch, }: CasesTableFiltersProps) => { const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map(r => r.full_name ?? r.username ?? '') @@ -48,8 +50,29 @@ const CasesTableFiltersComponent = ({ const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [showOpenCases, setShowOpenCases] = useState(initial.status === 'open'); - const { tags } = useGetTags(); - const { reporters, respReporters } = useGetReporters(); + const { tags, fetchTags } = useGetTags(); + const { reporters, respReporters, fetchReporters } = useGetReporters(); + const refetch = useCallback(() => { + fetchTags(); + fetchReporters(); + }, [fetchReporters, fetchTags]); + useEffect(() => { + if (setFilterRefetch != null) { + setFilterRefetch(refetch); + } + }, [refetch, setFilterRefetch]); + useEffect(() => { + if (selectedReporters.length) { + const newReporters = selectedReporters.filter(r => reporters.includes(r)); + handleSelectedReporters(newReporters); + } + }, [reporters]); + useEffect(() => { + if (selectedTags.length) { + const newTags = selectedTags.filter(t => tags.includes(t)); + handleSelectedTags(newTags); + } + }, [tags]); const handleSelectedReporters = useCallback( newReporters => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx index 8a25a2121104d..8b6ee76dd783d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { TestProviders } from '../../../../mock'; -import { basicCase } from '../__mock__/case_data'; +import { basicCase } from '../../../../containers/case/mock'; import { CaseViewActions } from './actions'; jest.mock('../../../../containers/case/use_delete_cases'); const useDeleteCasesMock = useDeleteCases as jest.Mock; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index 3721a5a727ca5..7ce9d7b8533e4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -8,13 +8,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { CaseComponent, CaseView } from './'; -import { - basicCaseClosed, - caseClosedProps, - caseProps, - caseUserActions, -} from '../__mock__/case_data'; +import { CaseComponent, CaseProps, CaseView } from './'; +import { basicCase, basicCaseClosed, caseUserActions } from '../../../../containers/case/mock'; import { TestProviders } from '../../../../mock'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { useGetCase } from '../../../../containers/case/use_get_case'; @@ -29,6 +24,19 @@ const useUpdateCaseMock = useUpdateCase as jest.Mock; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; const usePushToServiceMock = usePushToService as jest.Mock; +export const caseProps: CaseProps = { + caseId: basicCase.id, + userCanCrud: true, + caseData: basicCase, + fetchCase: jest.fn(), + updateCase: jest.fn(), +}; + +export const caseClosedProps: CaseProps = { + ...caseProps, + caseData: basicCaseClosed, +}; + describe('CaseView ', () => { const updateCaseProperty = jest.fn(); const fetchCaseUserActions = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index 17132b9610754..70b8035db5c16 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -118,3 +118,6 @@ export const EMAIL_BODY = (caseUrl: string) => values: { caseUrl }, defaultMessage: 'Case reference: {caseUrl}', }); +export const UNKNOWN = i18n.translate('xpack.siem.case.caseView.unknown', { + defaultMessage: 'Unknown', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx index 5c342538f0feb..e34981286bc81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { getUserAction } from '../__mock__/case_data'; +import { getUserAction } from '../../../../containers/case/mock'; import { getLabelTitle } from './helpers'; import * as i18n from '../case_view/translations'; import { mount } from 'enzyme'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx index 0d8cd729b4a1d..1c71260422d4b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx @@ -10,7 +10,7 @@ import { mount } from 'enzyme'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; import { getFormMock } from '../__mock__/form'; import { useUpdateComment } from '../../../../containers/case/use_update_comment'; -import { basicCase, getUserAction } from '../__mock__/case_data'; +import { basicCase, getUserAction } from '../../../../containers/case/mock'; import { UserActionTree } from './'; import { TestProviders } from '../../../../mock'; import { useFormMock } from '../create/index.test'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index f8f3f0651fa3c..d1e8eb3f6306b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -179,7 +179,7 @@ export const UserActionTree = React.memo( markdown={MarkdownDescription} onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)} onQuote={handleManageQuote.bind(null, caseData.description)} - username={caseData.createdBy.username ?? 'Unknown'} + username={caseData.createdBy.username ?? i18n.UNKNOWN} /> {caseUserActions.map((action, index) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx index e2189367068ca..8a1e8a80f664d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; import copy from 'copy-to-clipboard'; import { Router, routeData, mockHistory } from '../__mock__/router'; -import { caseUserActions as basicUserActions } from '../__mock__/case_data'; +import { caseUserActions as basicUserActions } from '../../../../containers/case/mock'; import { UserActionTitle } from './user_action_title'; import { TestProviders } from '../../../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx index a1edbab7e1fa2..fc2a74466dedc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -43,7 +43,7 @@ interface UserActionTitleProps { linkId?: string | null; fullName?: string | null; updatedAt?: string | null; - username: string; + username?: string | null; onEdit?: (id: string) => void; onQuote?: (id: string) => void; outlineComment?: (id: string) => void; @@ -63,7 +63,7 @@ export const UserActionTitle = ({ onQuote, outlineComment, updatedAt, - username, + username = i18n.UNKNOWN, }: UserActionTitleProps) => { const { detailName: caseId } = useParams(); const urlSearch = useGetUrlSearch(navTabs.case); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx new file mode 100644 index 0000000000000..c5a4057b64ea7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ActivityMonitor } from './index'; + +describe('activity_monitor', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('[title="Activity monitor"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx new file mode 100644 index 0000000000000..a2685017f86d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineHeaderPage } from './index'; + +describe('detection_engine_header_page', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('[title="Title"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx new file mode 100644 index 0000000000000..0e2589150e858 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { NoApiIntegrationKeyCallOut } from './index'; + +describe('no_api_integration_callout', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiCallOut')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx new file mode 100644 index 0000000000000..2e6890e60fc61 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { NoWriteSignalsCallOut } from './index'; + +describe('no_write_signals_callout', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiCallOut')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx new file mode 100644 index 0000000000000..b66a9fc881045 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsTableComponent } from './index'; + +describe('SignalsTableComponent', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[title="Signals"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index 6cdb2f326901e..ce8ae2054b2c7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -61,7 +61,7 @@ interface OwnProps { type SignalsTableComponentProps = OwnProps & PropsFromRedux; -const SignalsTableComponent: React.FC = ({ +export const SignalsTableComponent: React.FC = ({ canUserCRUD, clearEventsDeleted, clearEventsLoading, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx new file mode 100644 index 0000000000000..dd30bb1b0a74d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsTableFilterGroup } from './index'; + +describe('SignalsTableFilterGroup', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiFilterButton')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx deleted file mode 100644 index bb45ff68cb01d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx +++ /dev/null @@ -1,92 +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 { EuiContextMenuItem } from '@elastic/eui'; -import React from 'react'; -import * as i18n from './translations'; -import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; - -interface GetBatchItems { - areEventsLoading: boolean; - allEventsSelected: boolean; - selectedEventIds: Readonly>; - updateSignalsStatus: UpdateSignalsStatus; - sendSignalsToTimeline: SendSignalsToTimeline; - closePopover: () => void; - isFilteredToOpen: boolean; -} -/** - * Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel - * - * @param areEventsLoading are any events loading - * @param allEventsSelected are all events on all pages selected - * @param selectedEventIds - * @param updateSignalsStatus function for updating signal status - * @param sendSignalsToTimeline function for sending signals to timeline - * @param closePopover - * @param isFilteredToOpen currently selected filter options - */ -export const getBatchItems = ({ - areEventsLoading, - allEventsSelected, - selectedEventIds, - updateSignalsStatus, - sendSignalsToTimeline, - closePopover, - isFilteredToOpen, -}: GetBatchItems) => { - const allDisabled = areEventsLoading || Object.keys(selectedEventIds).length === 0; - const sendToTimelineDisabled = allEventsSelected || uniqueRuleCount(selectedEventIds) > 1; - const filterString = isFilteredToOpen - ? i18n.BATCH_ACTION_CLOSE_SELECTED - : i18n.BATCH_ACTION_OPEN_SELECTED; - - return [ - { - closePopover(); - sendSignalsToTimeline(); - }} - > - {i18n.BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE} - , - - { - closePopover(); - await updateSignalsStatus({ - signalIds: Object.keys(selectedEventIds), - status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, - }); - }} - > - {filterString} - , - ]; -}; - -/** - * Returns the number of unique rules for a given list of signals - * - * @param signals - */ -export const uniqueRuleCount = ( - signals: Readonly> -): number => { - const ruleIds = Object.values(signals).flatMap( - data => data.find(d => d.field === 'signal.rule.id')?.value - ); - - return Array.from(new Set(ruleIds)).length; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx new file mode 100644 index 0000000000000..6cab43b5285b5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.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 { shallow } from 'enzyme'; + +import { SignalsUtilityBar } from './index'; + +jest.mock('../../../../../lib/kibana'); + +describe('SignalsUtilityBar', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[dataTestSubj="openCloseSignal"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx new file mode 100644 index 0000000000000..6921c49d8a8b4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsHistogramPanel } from './index'; + +jest.mock('../../../../lib/kibana'); +jest.mock('../../../../components/navigation/use_get_url_search'); + +describe('SignalsHistogramPanel', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx new file mode 100644 index 0000000000000..5eb9beaaaf76a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsHistogram } from './signals_histogram'; + +jest.mock('../../../../lib/kibana'); + +describe('SignalsHistogram', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('Chart')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx new file mode 100644 index 0000000000000..b3d710de5e94e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { renderHook } from '@testing-library/react-hooks'; +import { useUserInfo } from './index'; + +import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; +import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; +import { useKibana } from '../../../../lib/kibana'; +jest.mock('../../../../containers/detection_engine/signals/use_privilege_user'); +jest.mock('../../../../containers/detection_engine/signals/use_signal_index'); +jest.mock('../../../../lib/kibana'); + +describe('useUserInfo', () => { + beforeAll(() => { + (usePrivilegeUser as jest.Mock).mockReturnValue({}); + (useSignalIndex as jest.Mock).mockReturnValue({}); + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, + }, + }); + }); + it('returns default state', () => { + const { result } = renderHook(() => useUserInfo()); + + expect(result).toEqual({ + current: { + canUserCRUD: null, + hasEncryptionKey: null, + hasIndexManage: null, + hasIndexWrite: null, + isAuthenticated: null, + isSignalIndexExists: null, + loading: true, + signalIndexName: null, + }, + error: undefined, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx new file mode 100644 index 0000000000000..779e9a4557f2a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { useParams } from 'react-router-dom'; + +import '../../mock/match_media'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { DetectionEnginePageComponent } from './detection_engine'; +import { useUserInfo } from './components/user_info'; + +jest.mock('./components/user_info'); +jest.mock('../../lib/kibana'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('DetectionEnginePageComponent', () => { + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({}); + (useUserInfo as jest.Mock).mockReturnValue({}); + }); + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('WithSource')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 1bd7ab2c4f1ae..a26d7f5672106 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -59,7 +59,7 @@ const detectionsTabs: Record = { }, }; -const DetectionEnginePageComponent: React.FC = ({ +export const DetectionEnginePageComponent: React.FC = ({ filters, query, setAbsoluteRangeDatePicker, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx new file mode 100644 index 0000000000000..f64526fd2f7c4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; +jest.mock('../../lib/kibana'); + +describe('DetectionEngineEmptyPage', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EmptyPage')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx new file mode 100644 index 0000000000000..e9f05f7aafe3c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; +jest.mock('../../lib/kibana'); + +describe('DetectionEngineNoIndex', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EmptyPage')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx new file mode 100644 index 0000000000000..e71f4de2b010b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; +jest.mock('../../lib/kibana'); + +describe('DetectionEngineUserUnauthenticated', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EmptyPage')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts deleted file mode 100644 index 1399df0fcf6d1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts +++ /dev/null @@ -1,18 +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 const sampleChartOptions = [ - { text: 'Risk scores', value: 'risk_scores' }, - { text: 'Severities', value: 'severities' }, - { text: 'Top destination IPs', value: 'destination_ips' }, - { text: 'Top event actions', value: 'event_actions' }, - { text: 'Top event categories', value: 'event_categories' }, - { text: 'Top host names', value: 'host_names' }, - { text: 'Top rule types', value: 'rule_types' }, - { text: 'Top rules', value: 'rules' }, - { text: 'Top source IPs', value: 'source_ips' }, - { text: 'Top users', value: 'users' }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx new file mode 100644 index 0000000000000..6c4980f1d1500 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import '../../mock/match_media'; +import { DetectionEngineContainer } from './index'; + +describe('DetectionEngineContainer', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('ManageUserInfo')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx new file mode 100644 index 0000000000000..f4955c2a93b8d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AllRules } from './index'; + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); + +describe('AllRules', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[title="All rules"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx new file mode 100644 index 0000000000000..92f69d79110d2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RulesTableFilters } from './rules_table_filters'; + +describe('RulesTableFilters', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="show-elastic-rules-filter-button"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx new file mode 100644 index 0000000000000..e31b8394e07d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TagsFilterPopover } from './tags_filter_popover'; + +describe('TagsFilterPopover', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('EuiPopover')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx new file mode 100644 index 0000000000000..9202da3336565 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AccordionTitle } from './index'; + +describe('AccordionTitle', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('h6').text()).toContain('title'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx new file mode 100644 index 0000000000000..eafa89a33f596 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.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 { shallow } from 'enzyme'; + +import { AddItem } from './index'; +import { useFormFieldMock } from '../../../../../../public/mock/test_providers'; + +describe('AddItem', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[iconType="plusInCircle"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx new file mode 100644 index 0000000000000..3dab83bca2946 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useRef } from 'react'; +import { shallow } from 'enzyme'; + +import { AllRulesTables } from './index'; + +describe('AllRulesTables', () => { + it('renders correctly', () => { + const Component = () => { + const ref = useRef(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx new file mode 100644 index 0000000000000..c0e957d94261f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AnomalyThresholdSlider } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('AnomalyThresholdSlider', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('EuiRange')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx index 19d1c698cbd9b..01fddf98b97d8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx @@ -16,10 +16,10 @@ interface AnomalyThresholdSliderProps { type Event = React.ChangeEvent; type EventArg = Event | React.MouseEvent; -export const AnomalyThresholdSlider: React.FC = ({ +export const AnomalyThresholdSlider = ({ describedByIds = [], field, -}) => { +}: AnomalyThresholdSliderProps) => { const threshold = field.value as number; const onThresholdChange = useCallback( (event: EventArg) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx new file mode 100644 index 0000000000000..59231c31d15bb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.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 { shallow } from 'enzyme'; + +import { MlJobDescription, AuditIcon, JobStatusBadge } from './ml_job_description'; +jest.mock('../../../../../lib/kibana'); + +const job = { + moduleId: 'moduleId', + defaultIndexPattern: 'defaultIndexPattern', + isCompatible: true, + isInstalled: true, + isElasticJob: true, + datafeedId: 'datafeedId', + datafeedIndices: [], + datafeedState: 'datafeedState', + description: 'description', + groups: [], + hasDatafeed: true, + id: 'id', + isSingleMetricViewerJob: false, + jobState: 'jobState', + memory_status: 'memory_status', + processed_record_count: 0, +}; + +describe('MlJobDescription', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1); + }); +}); + +describe('AuditIcon', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiToolTip')).toHaveLength(0); + }); +}); + +describe('JobStatusBadge', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiBadge')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx index 5a9593f1a6de2..1664ea320bc1e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx @@ -20,7 +20,7 @@ enum MessageLevels { error = 'error', } -const AuditIcon: React.FC<{ +const AuditIconComponent: React.FC<{ message: SiemJob['auditMessage']; }> = ({ message }) => { if (!message) { @@ -45,7 +45,9 @@ const AuditIcon: React.FC<{ ); }; -export const JobStatusBadge: React.FC<{ job: SiemJob }> = ({ job }) => { +export const AuditIcon = React.memo(AuditIconComponent); + +const JobStatusBadgeComponent: React.FC<{ job: SiemJob }> = ({ job }) => { const isStarted = isJobStarted(job.jobState, job.datafeedState); const color = isStarted ? 'secondary' : 'danger'; const text = isStarted ? ML_JOB_STARTED : ML_JOB_STOPPED; @@ -57,6 +59,8 @@ export const JobStatusBadge: React.FC<{ job: SiemJob }> = ({ job }) => { ); }; +export const JobStatusBadge = React.memo(JobStatusBadgeComponent); + const JobLink = styled(EuiLink)` margin-right: ${({ theme }) => theme.eui.euiSizeS}; `; @@ -65,7 +69,7 @@ const Wrapper = styled.div` overflow: hidden; `; -export const MlJobDescription: React.FC<{ job: SiemJob }> = ({ job }) => { +const MlJobDescriptionComponent: React.FC<{ job: SiemJob }> = ({ job }) => { const jobUrl = useKibana().services.application.getUrlForApp( `ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})` ); @@ -83,6 +87,8 @@ export const MlJobDescription: React.FC<{ job: SiemJob }> = ({ job }) => { ); }; +export const MlJobDescription = React.memo(MlJobDescriptionComponent); + export const buildMlJobDescription = ( jobId: string, label: string, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx new file mode 100644 index 0000000000000..dc201eb21c911 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isMitreAttackInvalid } from './helpers'; + +describe('isMitreAttackInvalid', () => { + it('returns true if tacticName is empty', () => { + expect(isMitreAttackInvalid('', undefined)).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx new file mode 100644 index 0000000000000..3e8d542682456 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.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 { shallow } from 'enzyme'; + +import { AddMitreThreat } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('AddMitreThreat', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="addMitre"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx new file mode 100644 index 0000000000000..dea27d8d04536 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.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 { shallow } from 'enzyme'; + +import { MlJobSelect } from './index'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; +import { useFormFieldMock } from '../../../../../mock'; +jest.mock('../../../../../components/ml_popover/hooks/use_siem_jobs'); +jest.mock('../../../../../lib/kibana'); + +describe('MlJobSelect', () => { + beforeAll(() => { + (useSiemJobs as jest.Mock).mockReturnValue([false, []]); + }); + + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="mlJobSelect"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx new file mode 100644 index 0000000000000..789f12f290a34 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx @@ -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 { shallow } from 'enzyme'; + +import { OptionalFieldLabel } from './index'; + +describe('OptionalFieldLabel', () => { + it('renders correctly', () => { + const wrapper = shallow(OptionalFieldLabel); + + expect(wrapper.find('EuiTextColor')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx new file mode 100644 index 0000000000000..fefc9697176c4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.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 { shallow } from 'enzyme'; + +import { PickTimeline } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('PickTimeline', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="pick-timeline"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx new file mode 100644 index 0000000000000..8ace42fc5c3f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { PrePackagedRulesPrompt } from './load_empty_prompt'; + +describe('PrePackagedRulesPrompt', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('EmptyPrompt')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx index 1cff4751e8188..5d136265ef1f2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx @@ -15,6 +15,8 @@ const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ `; +EmptyPrompt.displayName = 'EmptyPrompt'; + interface PrePackagedRulesPromptProps { createPrePackagedRules: () => void; loading: boolean; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx new file mode 100644 index 0000000000000..807da79fb7a1a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UpdatePrePackagedRulesCallOut } from './update_callout'; +import { useKibana } from '../../../../../lib/kibana'; +jest.mock('../../../../../lib/kibana'); + +describe('UpdatePrePackagedRulesCallOut', () => { + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + docLinks: { + ELASTIC_WEBSITE_URL: '', + DOC_LINK_VERSION: '', + }, + }, + }); + }); + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx new file mode 100644 index 0000000000000..cdd06ad58bb4b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.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 { shallow } from 'enzyme'; + +import { QueryBarDefineRule } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +jest.mock('../../../../../lib/kibana'); + +describe('QueryBarDefineRule', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="query-bar-define-rule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx new file mode 100644 index 0000000000000..e761cb3323b2c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ReadOnlyCallOut } from './index'; + +describe('ReadOnlyCallOut', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx new file mode 100644 index 0000000000000..4cfad36b2933f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.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 { shallow } from 'enzyme'; + +import { RuleActionsField } from './index'; +import { useKibana } from '../../../../../lib/kibana'; +import { useFormFieldMock } from '../../../../../mock'; +jest.mock('../../../../../lib/kibana'); + +describe('RuleActionsField', () => { + it('should not render ActionForm is no actions are supported', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + triggers_actions_ui: { + actionTypeRegistry: {}, + }, + }, + }); + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('ActionForm')).toHaveLength(0); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx new file mode 100644 index 0000000000000..aba30e4b7f3ca --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getStatusColor } from './helpers'; + +describe('rule_status helpers', () => { + it('getStatusColor returns subdued if null was provided', () => { + expect(getStatusColor(null)).toBe('subdued'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx new file mode 100644 index 0000000000000..6e230de11c4f3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RuleStatus } from './index'; + +describe('RuleStatus', () => { + it('renders loader correctly', () => { + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="rule-status-loader"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx new file mode 100644 index 0000000000000..3829af02ca4f1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.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 { shallow } from 'enzyme'; + +import { ScheduleItem } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('ScheduleItem', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="schedule-item"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx new file mode 100644 index 0000000000000..3d832d61abb28 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SelectRuleType } from './index'; +import { useFormFieldMock } from '../../../../../mock'; +jest.mock('../../../../../lib/kibana'); + +describe('SelectRuleType', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="selectRuleType"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx new file mode 100644 index 0000000000000..a9dddfedc2bab --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SeverityBadge } from './index'; + +describe('SeverityBadge', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiHealth')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx new file mode 100644 index 0000000000000..89b8a56e79054 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../../mock'; +import { RuleStatusIcon } from './index'; +jest.mock('../../../../../lib/kibana'); + +describe('RuleStatusIcon', () => { + it('renders correctly', () => { + const wrapper = shallow(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('EuiAvatar')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx new file mode 100644 index 0000000000000..af0547ea03261 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { StepContentWrapper } from './index'; + +describe('StepContentWrapper', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('div')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx index b04a321dab05b..a7343a87a7ef8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx @@ -16,3 +16,5 @@ StyledDiv.defaultProps = { }; export const StepContentWrapper = React.memo(StyledDiv); + +StepContentWrapper.displayName = 'StepContentWrapper'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx new file mode 100644 index 0000000000000..ebef6348d477e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { StepDefineRule } from './index'; + +jest.mock('../../../../../lib/kibana'); + +describe('StepDefineRule', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('Form[data-test-subj="stepDefineRule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx new file mode 100644 index 0000000000000..ce01d6995a2d8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { StepPanel } from './index'; + +describe('StepPanel', () => { + it('renders correctly', () => { + const wrapper = shallow( + +
+ + ); + + expect(wrapper.find('MyPanel')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx index 88cecadb8b137..1923ed09252dd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx @@ -20,6 +20,8 @@ const MyPanel = styled(EuiPanel)` position: relative; `; +MyPanel.displayName = 'MyPanel'; + const StepPanelComponent: React.FC = ({ children, loading, title }) => ( {loading && } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx new file mode 100644 index 0000000000000..69d118ba9f28e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { StepRuleActions } from './index'; + +jest.mock('../../../../../lib/kibana'); + +describe('StepRuleActions', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="stepRuleActions"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx index bc3b0dfe720bc..1b27d0e0fcc0e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* istanbul ignore file */ + import { i18n } from '@kbn/i18n'; import { FormSchema } from '../../../../../shared_imports'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx new file mode 100644 index 0000000000000..98de933590d60 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { TestProviders } from '../../../../../mock'; +import { StepScheduleRule } from './index'; + +describe('StepScheduleRule', () => { + it('renders correctly', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('Form[data-test-subj="stepScheduleRule"]')).toHaveLength(1); + }); + + it('renders correctly if isReadOnlyView', () => { + const wrapper = shallow(); + + expect(wrapper.find('StepContentWrapper')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 8fbfdf5f25a51..e79aec2be6e15 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* istanbul ignore file */ + import { i18n } from '@kbn/i18n'; import { OptionalFieldLabel } from '../optional_field_label'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx new file mode 100644 index 0000000000000..0ab19b671494e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ThrottleSelectField } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('ThrottleSelectField', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('SelectField')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx new file mode 100644 index 0000000000000..db32be652d0f7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { CreateRulePage } from './index'; +import { useUserInfo } from '../../components/user_info'; + +jest.mock('../../components/user_info'); + +describe('CreateRulePage', () => { + it('renders correctly', () => { + (useUserInfo as jest.Mock).mockReturnValue({}); + const wrapper = shallow(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx new file mode 100644 index 0000000000000..a83ff4c54b076 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { FailureHistory } from './failure_history'; +import { useRuleStatus } from '../../../../containers/detection_engine/rules'; +jest.mock('../../../../containers/detection_engine/rules'); + +describe('FailureHistory', () => { + beforeAll(() => { + (useRuleStatus as jest.Mock).mockReturnValue([false, null]); + }); + + it('renders correctly', () => { + const wrapper = shallow(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx new file mode 100644 index 0000000000000..19c6f39a9bc7e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.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 { shallow } from 'enzyme'; + +import '../../../../mock/match_media'; +import { TestProviders } from '../../../../mock'; +import { RuleDetailsPageComponent } from './index'; +import { setAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; +import { useUserInfo } from '../../components/user_info'; +import { useParams } from 'react-router-dom'; + +jest.mock('../../components/user_info'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('RuleDetailsPageComponent', () => { + beforeAll(() => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (useParams as jest.Mock).mockReturnValue({}); + }); + + it('renders correctly', () => { + const wrapper = shallow( + , + { + wrappingComponent: TestProviders, + } + ); + + expect(wrapper.find('WithSource')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 2b648a3b3f825..14e5f2b90882e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -88,7 +88,7 @@ const ruleDetailTabs = [ }, ]; -const RuleDetailsPageComponent: FC = ({ +export const RuleDetailsPageComponent: FC = ({ filters, query, setAbsoluteRangeDatePicker, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx new file mode 100644 index 0000000000000..3394b0fc8c5c0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RuleStatusFailedCallOut } from './status_failed_callout'; + +describe('RuleStatusFailedCallOut', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx new file mode 100644 index 0000000000000..d22bc12abf9fa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.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 { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { EditRulePage } from './index'; +import { useUserInfo } from '../../components/user_info'; +import { useParams } from 'react-router-dom'; + +jest.mock('../../components/user_info'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('EditRulePage', () => { + it('renders correctly', () => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (useParams as jest.Mock).mockReturnValue({}); + const wrapper = shallow(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[title="Edit rule settings"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index 443dbd2c93a35..1c01a19573cd6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -302,11 +302,25 @@ describe('rule helpers', () => { test('returns expected ActionsStepRule rule object', () => { const mockedRule = { ...mockRule('test-id'), - actions: [], + actions: [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + }, + ], }; const result: ActionsStepRule = getActionsStepsData(mockedRule); const expected = { - actions: [], + actions: [ + { + id: 'id', + group: 'group', + params: {}, + actionTypeId: 'action_type_id', + }, + ], enabled: mockedRule.enabled, isNew: false, throttle: 'no_actions', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx new file mode 100644 index 0000000000000..3fa81ca3ced08 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RulesPage } from './index'; +import { useUserInfo } from '../components/user_info'; +import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; + +jest.mock('../components/user_info'); +jest.mock('../../../containers/detection_engine/rules'); + +describe('RulesPage', () => { + beforeAll(() => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (usePrePackagedRules as jest.Mock).mockReturnValue({}); + }); + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('AllRules')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts new file mode 100644 index 0000000000000..34a521ed32b12 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getBreadcrumbs } from './utils'; + +describe('getBreadcrumbs', () => { + it('returns default value for incorrect params', () => { + expect( + getBreadcrumbs( + { + pageName: 'pageName', + detailName: 'detailName', + tabName: undefined, + search: '', + pathName: 'pathName', + }, + [] + ) + ).toEqual([{ href: '#/link-to/detections', text: 'Detections' }]); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 29ae5056a3ae8..57ccc7a7806ac 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -26,13 +26,13 @@ import { buildSiemResponse, validateLicenseForRuleType, } from '../utils'; -import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; import { ImportRulesSchema, importRulesSchema } from '../schemas/response/import_rules_schema'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; import { validate } from './validate'; +import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; type PromiseFromStreams = ImportRuleAlertRest | Error; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 31a0f37fe81c9..8b1b0cab3b2f2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -12,7 +12,6 @@ import { transformTags, getIdBulkError, transformOrBulkError, - transformDataToNdjson, transformAlertsToRules, transformOrImportError, getDuplicates, @@ -22,14 +21,13 @@ import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; -import { sampleRule } from '../../signals/__mocks__/es_results'; import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; -import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { PartialAlert } from '../../../../../../../../plugins/alerting/server'; import { SanitizedAlert } from '../../../../../../../../plugins/alerting/server/types'; import { RuleAlertType } from '../../rules/types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -396,47 +394,6 @@ describe('utils', () => { }); }); - describe('transformDataToNdjson', () => { - test('if rules are empty it returns an empty string', () => { - const ruleNdjson = transformDataToNdjson([]); - expect(ruleNdjson).toEqual(''); - }); - - test('single rule will transform with new line ending character for ndjson', () => { - const rule = sampleRule(); - const ruleNdjson = transformDataToNdjson([rule]); - expect(ruleNdjson.endsWith('\n')).toBe(true); - }); - - test('multiple rules will transform with two new line ending characters for ndjson', () => { - const result1 = sampleRule(); - const result2 = sampleRule(); - result2.id = 'some other id'; - result2.rule_id = 'some other id'; - result2.name = 'Some other rule'; - - const ruleNdjson = transformDataToNdjson([result1, result2]); - // this is how we count characters in JavaScript :-) - const count = ruleNdjson.split('\n').length - 1; - expect(count).toBe(2); - }); - - test('you can parse two rules back out without errors', () => { - const result1 = sampleRule(); - const result2 = sampleRule(); - result2.id = 'some other id'; - result2.rule_id = 'some other id'; - result2.name = 'Some other rule'; - - const ruleNdjson = transformDataToNdjson([result1, result2]); - const ruleStrings = ruleNdjson.split('\n'); - const reParsed1 = JSON.parse(ruleStrings[0]); - const reParsed2 = JSON.parse(ruleStrings[1]); - expect(reParsed1).toEqual(result1); - expect(reParsed2).toEqual(result2); - }); - }); - describe('transformAlertsToRules', () => { test('given an empty array returns an empty array', () => { expect(transformAlertsToRules([])).toEqual([]); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 4d13fa1b6ae50..790603fa8cfc1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -152,15 +152,6 @@ export const transformAlertToRule = ( }); }; -export const transformDataToNdjson = (data: unknown[]): string => { - if (data.length !== 0) { - const dataString = data.map(rule => JSON.stringify(rule)).join('\n'); - return `${dataString}\n`; - } else { - return ''; - } -}; - export const transformAlertsToRules = ( alerts: RuleAlertType[] ): Array> => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index 3e22999528101..27008d17d2192 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -4,39 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { Transform } from 'stream'; -import { has, isString } from 'lodash/fp'; import { ImportRuleAlertRest } from '../types'; import { createSplitStream, createMapStream, - createFilterStream, createConcatStream, } from '../../../../../../../../src/legacy/utils/streams'; import { importRulesSchema } from '../routes/schemas/import_rules_schema'; import { BadRequestError } from '../errors/bad_request_error'; - -export interface RulesObjectsExportResultDetails { - /** number of successfully exported objects */ - exportedCount: number; -} - -export const parseNdjsonStrings = (): Transform => { - return createMapStream((ndJsonStr: string) => { - if (isString(ndJsonStr) && ndJsonStr.trim() !== '') { - try { - return JSON.parse(ndJsonStr); - } catch (err) { - return err; - } - } - }); -}; - -export const filterExportedCounts = (): Transform => { - return createFilterStream( - obj => obj != null && !has('exported_count', obj) - ); -}; +import { + parseNdjsonStrings, + filterExportedCounts, + createLimitStream, +} from '../../../utils/read_stream/create_stream_from_ndjson'; export const validateRules = (): Transform => { return createMapStream((obj: ImportRuleAlertRest) => { @@ -53,21 +33,6 @@ export const validateRules = (): Transform => { }); }; -// Adaptation from: saved_objects/import/create_limit_stream.ts -export const createLimitStream = (limit: number): Transform => { - let counter = 0; - return new Transform({ - objectMode: true, - async transform(obj, _, done) { - if (counter >= limit) { - return done(new Error(`Can't import more than ${limit} rules`)); - } - counter++; - done(undefined, obj); - }, - }); -}; - // TODO: Capture both the line number and the rule_id if you have that information for the error message // eventually and then pass it down so we can give error messages on the line number diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts index 6a27abb66ce85..40c07f28ea848 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts @@ -7,7 +7,8 @@ import { AlertsClient } from '../../../../../../../plugins/alerting/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; -import { transformAlertsToRules, transformDataToNdjson } from '../routes/rules/utils'; +import { transformAlertsToRules } from '../routes/rules/utils'; +import { transformDataToNdjson } from '../../../utils/read_stream/create_stream_from_ndjson'; export const getExportAll = async ( alertsClient: AlertsClient diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 6f642231ebbaf..048f09e95b062 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -8,8 +8,9 @@ import { AlertsClient } from '../../../../../../../plugins/alerting/server'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { isAlertType } from '../rules/types'; import { readRules } from './read_rules'; -import { transformDataToNdjson, transformAlertToRule } from '../routes/rules/utils'; +import { transformAlertToRule } from '../routes/rules/utils'; import { OutputRuleAlertRest } from '../types'; +import { transformDataToNdjson } from '../../../utils/read_stream/create_stream_from_ndjson'; interface ExportSuccesRule { statusCode: 200; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts index 5373570a4f8cc..16654b2863ee5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts @@ -3,8 +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 * as rt from 'io-ts'; import { Transform } from 'stream'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { failure } from 'io-ts/lib/PathReporter'; +import { identity } from 'fp-ts/lib/function'; import { createConcatStream, createSplitStream, @@ -14,26 +18,28 @@ import { parseNdjsonStrings, filterExportedCounts, createLimitStream, -} from '../detection_engine/rules/create_rules_stream_from_ndjson'; -import { importTimelinesSchema } from './routes/schemas/import_timelines_schema'; -import { BadRequestError } from '../detection_engine/errors/bad_request_error'; +} from '../../utils/read_stream/create_stream_from_ndjson'; + import { ImportTimelineResponse } from './routes/utils/import_timelines'; +import { ImportTimelinesSchemaRt } from './routes/schemas/import_timelines_schema'; + +type ErrorFactory = (message: string) => Error; -export const validateTimelines = (): Transform => { - return createMapStream((obj: ImportTimelineResponse) => { - if (!(obj instanceof Error)) { - const validated = importTimelinesSchema.validate(obj); - if (validated.error != null) { - return new BadRequestError(validated.error.message); - } else { - return validated.value; - } - } else { - return obj; - } - }); +export const createPlainError = (message: string) => new Error(message); + +export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { + throw createError(failure(errors).join('\n')); }; +export const decodeOrThrow = ( + runtimeType: rt.Type, + createError: ErrorFactory = createPlainError +) => (inputValue: I) => + pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); + +export const validateTimelines = (): Transform => + createMapStream((obj: ImportTimelineResponse) => decodeOrThrow(ImportTimelinesSchemaRt)(obj)); + export const createTimelinesStreamFromNdJson = (ruleLimit: number) => { return [ createSplitStream('\n'), diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts index 0e73e4bdd6c97..a83c443773302 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -6,11 +6,16 @@ import { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL } from '../../../../../common/constants'; import { requestMock } from '../../../detection_engine/routes/__mocks__'; - +import stream from 'stream'; +const readable = new stream.Readable(); export const getExportTimelinesRequest = () => requestMock.create({ method: 'get', path: TIMELINE_EXPORT_URL, + query: { + file_name: 'mock_export_timeline.ndjson', + exclude_export_details: 'false', + }, body: { ids: ['f0e58720-57b6-11ea-b88d-3f1a31716be8', '890b8ae0-57df-11ea-a7c9-3976b7f1cb37'], }, @@ -22,7 +27,7 @@ export const getImportTimelinesRequest = (filename?: string) => path: TIMELINE_IMPORT_URL, query: { overwrite: false }, body: { - file: { hapi: { filename: filename ?? 'filename.ndjson' } }, + file: { ...readable, hapi: { filename: filename ?? 'filename.ndjson' } }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts index fe434b5399212..4eadede40f5d9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts @@ -83,7 +83,7 @@ describe('export timelines', () => { }); describe('request validation', () => { - test('disallows singular id query param', async () => { + test('return validation error for request body', async () => { const request = requestMock.create({ method: 'get', path: TIMELINE_EXPORT_URL, @@ -91,7 +91,26 @@ describe('export timelines', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith('"id" is not allowed'); + expect(result.badRequest.mock.calls[0][0]).toEqual( + 'Invalid value undefined supplied to : { ids: Array }/ids: Array' + ); + }); + + test('return validation error for request params', async () => { + const request = requestMock.create({ + method: 'get', + path: TIMELINE_EXPORT_URL, + body: { id: 'someId' }, + }); + const result = server.validate(request); + + expect(result.badRequest.mock.calls[1][0]).toEqual( + [ + 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/file_name: string', + 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/0: "true"', + 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/1: "false"', + ].join('\n') + ); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts index b8e7be13fff34..fa849c1c325a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts @@ -7,31 +7,24 @@ import { set as _set } from 'lodash/fp'; import { IRouter } from '../../../../../../../../src/core/server'; import { LegacyServices } from '../../../types'; -import { ExportTimelineRequestParams } from '../types'; -import { - transformError, - buildRouteValidation, - buildSiemResponse, -} from '../../detection_engine/routes/utils'; +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; +import { getExportTimelineByObjectIds } from './utils/export_timelines'; import { - exportTimelinesSchema, exportTimelinesQuerySchema, + exportTimelinesRequestBodySchema, } from './schemas/export_timelines_schema'; - -import { getExportTimelineByObjectIds } from './utils/export_timelines'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; export const exportTimelinesRoute = (router: IRouter, config: LegacyServices['config']) => { router.post( { path: TIMELINE_EXPORT_URL, validate: { - query: buildRouteValidation( - exportTimelinesQuerySchema - ), - body: buildRouteValidation(exportTimelinesSchema), + query: buildRouteValidation(exportTimelinesQuerySchema), + body: buildRouteValidation(exportTimelinesRequestBodySchema), }, options: { tags: ['access:siem'], @@ -42,6 +35,7 @@ export const exportTimelinesRoute = (router: IRouter, config: LegacyServices['co const siemResponse = buildSiemResponse(response); const savedObjectsClient = context.core.savedObjects.client; const exportSizeLimit = config().get('savedObjects.maxImportExportSize'); + if (request.body?.ids != null && request.body.ids.length > exportSizeLimit) { return siemResponse.error({ statusCode: 400, @@ -51,7 +45,7 @@ export const exportTimelinesRoute = (router: IRouter, config: LegacyServices['co const responseBody = await getExportTimelineByObjectIds({ client: savedObjectsClient, - request, + ids: request.body.ids, }); return response.ok({ diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index e89aef4c70ecb..352f8f0a355fc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -334,7 +334,10 @@ describe('import timelines', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "file" fails because ["file" is required]' + [ + 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/0: ReadableRt', + 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/1: { hapi: { filename: string } }', + ].join('\n') ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 2b41b4e7843a7..ad7ee28d8ad51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -6,8 +6,8 @@ import { extname } from 'path'; import { chunk, omit, set } from 'lodash/fp'; + import { - buildRouteValidation, buildSiemResponse, createBulkErrorObject, BulkError, @@ -23,7 +23,6 @@ import { isBulkError, isImportRegular, ImportTimelineResponse, - ImportTimelinesRequestParams, ImportTimelinesSchema, PromiseFromStreams, } from './utils/import_timelines'; @@ -31,14 +30,14 @@ import { import { IRouter } from '../../../../../../../../src/core/server'; import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; import { SetupPlugins } from '../../../plugin'; -import { importTimelinesPayloadSchema } from './schemas/import_timelines_schema'; +import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; import { LegacyServices } from '../../../types'; import { Timeline } from '../saved_object'; import { validate } from '../../detection_engine/routes/rules/validate'; import { FrameworkRequest } from '../../framework'; - +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; const CHUNK_PARSED_OBJECT_SIZE = 10; const timelineLib = new Timeline(); @@ -52,9 +51,7 @@ export const importTimelinesRoute = ( { path: `${TIMELINE_IMPORT_URL}`, validate: { - body: buildRouteValidation( - importTimelinesPayloadSchema - ), + body: buildRouteValidation(ImportTimelinesPayloadSchemaRt), }, options: { tags: ['access:siem'], @@ -65,28 +62,30 @@ export const importTimelinesRoute = ( }, }, async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - const savedObjectsClient = context.core.savedObjects.client; - if (!savedObjectsClient) { - return siemResponse.error({ statusCode: 404 }); - } - const { filename } = request.body.file.hapi; + try { + const siemResponse = buildSiemResponse(response); + const savedObjectsClient = context.core.savedObjects.client; + if (!savedObjectsClient) { + return siemResponse.error({ statusCode: 404 }); + } - const fileExtension = extname(filename).toLowerCase(); + const { file } = request.body; + const { filename } = file.hapi; - if (fileExtension !== '.ndjson') { - return siemResponse.error({ - statusCode: 400, - body: `Invalid file extension ${fileExtension}`, - }); - } + const fileExtension = extname(filename).toLowerCase(); - const objectLimit = config().get('savedObjects.maxImportExportSize'); + if (fileExtension !== '.ndjson') { + return siemResponse.error({ + statusCode: 400, + body: `Invalid file extension ${fileExtension}`, + }); + } + + const objectLimit = config().get('savedObjects.maxImportExportSize'); - try { const readStream = createTimelinesStreamFromNdJson(objectLimit); const parsedObjects = await createPromiseFromStreams([ - request.body.file, + file, ...readStream, ]); const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueTimeline( @@ -215,6 +214,7 @@ export const importTimelinesRoute = ( } } catch (err) { const error = transformError(err); + const siemResponse = buildSiemResponse(response); return siemResponse.error({ body: error.message, diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts index 04edbbd7046c9..6f8265903b2a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; +import * as rt from 'io-ts'; -/* eslint-disable @typescript-eslint/camelcase */ -import { ids, exclude_export_details, file_name } from './schemas'; -/* eslint-disable @typescript-eslint/camelcase */ - -export const exportTimelinesSchema = Joi.object({ - ids, -}).min(1); +export const exportTimelinesQuerySchema = rt.type({ + file_name: rt.string, + exclude_export_details: rt.union([rt.literal('true'), rt.literal('false')]), +}); -export const exportTimelinesQuerySchema = Joi.object({ - file_name: file_name.default('export.ndjson'), - exclude_export_details: exclude_export_details.default(false), +export const exportTimelinesRequestBodySchema = rt.type({ + ids: rt.array(rt.string), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts index 61ffa9681c53a..056fdaf0d2515 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts @@ -3,55 +3,41 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { - columns, - created, - createdBy, - dataProviders, - dateRange, - description, - eventNotes, - eventType, - favorite, - filters, - globalNotes, - kqlMode, - kqlQuery, - savedObjectId, - savedQueryId, - sort, - title, - updated, - updatedBy, - version, - pinnedEventIds, -} from './schemas'; +import * as rt from 'io-ts'; -export const importTimelinesPayloadSchema = Joi.object({ - file: Joi.object().required(), -}); +import { Readable } from 'stream'; +import { either } from 'fp-ts/lib/Either'; +import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; +import { SavedTimelineRuntimeType } from '../../types'; + +export const ImportTimelinesSchemaRt = rt.intersection([ + SavedTimelineRuntimeType, + rt.type({ + savedObjectId: rt.string, + version: rt.string, + }), + rt.type({ + globalNotes, + eventNotes, + pinnedEventIds, + }), +]); -export const importTimelinesSchema = Joi.object({ - columns, - created, - createdBy, - dataProviders, - dateRange, - description, - eventNotes, - eventType, - filters, - favorite, - globalNotes, - kqlMode, - kqlQuery, - savedObjectId, - savedQueryId, - sort, - title, - updated, - updatedBy, - version, - pinnedEventIds, +const ReadableRt = new rt.Type( + 'ReadableRt', + (u): u is Readable => u instanceof Readable, + (u, c) => + either.chain(rt.object.validate(u, c), s => { + const d = s as Readable; + return d.readable ? rt.success(d) : rt.failure(u, c); + }), + a => a +); +export const ImportTimelinesPayloadSchemaRt = rt.type({ + file: rt.intersection([ + ReadableRt, + rt.type({ + hapi: rt.type({ filename: rt.string }), + }), + ]), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index fc87a775a9e68..71627363ef0f8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -3,156 +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 Joi from 'joi'; +import * as runtimeTypes from 'io-ts'; +import { unionWithNullType } from '../../../framework'; +import { SavedNoteRuntimeType } from '../../../note/types'; -const allowEmptyString = Joi.string().allow([null, '']); -const columnHeaderType = allowEmptyString; -export const created = Joi.number().allow(null); -export const createdBy = allowEmptyString; - -export const description = allowEmptyString; -export const end = Joi.number(); -export const eventId = allowEmptyString; -export const eventType = allowEmptyString; - -export const filters = Joi.array() - .items( - Joi.object({ - meta: Joi.object({ - alias: allowEmptyString, - controlledBy: allowEmptyString, - disabled: Joi.boolean().allow(null), - field: allowEmptyString, - formattedValue: allowEmptyString, - index: allowEmptyString, - key: allowEmptyString, - negate: Joi.boolean().allow(null), - params: allowEmptyString, - type: allowEmptyString, - value: allowEmptyString, - }), - exists: allowEmptyString, - match_all: allowEmptyString, - missing: allowEmptyString, - query: allowEmptyString, - range: allowEmptyString, - script: allowEmptyString, - }) - ) - .allow(null); - -const name = allowEmptyString; - -export const noteId = allowEmptyString; -export const note = allowEmptyString; - -export const start = Joi.number(); -export const savedQueryId = allowEmptyString; -export const savedObjectId = allowEmptyString; - -export const timelineId = allowEmptyString; -export const title = allowEmptyString; - -export const updated = Joi.number().allow(null); -export const updatedBy = allowEmptyString; -export const version = allowEmptyString; - -export const columns = Joi.array().items( - Joi.object({ - aggregatable: Joi.boolean().allow(null), - category: allowEmptyString, - columnHeaderType, - description, - example: allowEmptyString, - indexes: allowEmptyString, - id: allowEmptyString, - name, - placeholder: allowEmptyString, - searchable: Joi.boolean().allow(null), - type: allowEmptyString, - }).required() -); -export const dataProviders = Joi.array() - .items( - Joi.object({ - id: allowEmptyString, - name: allowEmptyString, - enabled: Joi.boolean().allow(null), - excluded: Joi.boolean().allow(null), - kqlQuery: allowEmptyString, - queryMatch: Joi.object({ - field: allowEmptyString, - displayField: allowEmptyString, - value: allowEmptyString, - displayValue: allowEmptyString, - operator: allowEmptyString, - }), - and: Joi.array() - .items( - Joi.object({ - id: allowEmptyString, - name, - enabled: Joi.boolean().allow(null), - excluded: Joi.boolean().allow(null), - kqlQuery: allowEmptyString, - queryMatch: Joi.object({ - field: allowEmptyString, - displayField: allowEmptyString, - value: allowEmptyString, - displayValue: allowEmptyString, - operator: allowEmptyString, - }).allow(null), - }) - ) - .allow(null), - }) - ) - .allow(null); -export const dateRange = Joi.object({ - start, - end, -}); -export const favorite = Joi.array().items( - Joi.object({ - keySearch: allowEmptyString, - fullName: allowEmptyString, - userName: allowEmptyString, - favoriteDate: Joi.number(), - }).allow(null) -); -const noteItem = Joi.object({ - noteId, - version, - eventId, - note, - timelineId, - created, - createdBy, - updated, - updatedBy, -}); -export const eventNotes = Joi.array().items(noteItem); -export const globalNotes = Joi.array().items(noteItem); -export const kqlMode = allowEmptyString; -export const kqlQuery = Joi.object({ - filterQuery: Joi.object({ - kuery: Joi.object({ - kind: allowEmptyString, - expression: allowEmptyString, - }).allow(null), - serializedQuery: allowEmptyString, - }).allow(null), -}); -export const pinnedEventIds = Joi.array() - .items(allowEmptyString) - .allow(null); -export const sort = Joi.object({ - columnId: allowEmptyString, - sortDirection: allowEmptyString, -}); -/* eslint-disable @typescript-eslint/camelcase */ - -export const ids = Joi.array().items(allowEmptyString); - -export const exclude_export_details = Joi.boolean(); -export const file_name = allowEmptyString; +export const eventNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); +export const globalNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); +export const pinnedEventIds = runtimeTypes.array(unionWithNullType(runtimeTypes.string)); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index 8a28100fbae82..52ee2a891c9bb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -26,12 +26,11 @@ import { import { ExportedTimelines, ExportTimelineSavedObjectsClient, - ExportTimelineRequest, ExportedNotes, TimelineSavedObject, } from '../../types'; +import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; -import { transformDataToNdjson } from '../../../detection_engine/routes/rules/utils'; export type TimelineSavedObjectsClient = Pick< SavedObjectsClient, | 'get' @@ -142,23 +141,17 @@ const getTimelines = async ( const getTimelinesFromObjects = async ( savedObjectsClient: ExportTimelineSavedObjectsClient, - request: ExportTimelineRequest + ids: string[] ): Promise => { - const timelines: TimelineSavedObject[] = await getTimelines(savedObjectsClient, request.body.ids); + const timelines: TimelineSavedObject[] = await getTimelines(savedObjectsClient, ids); // To Do for feature freeze // if (timelines.length !== request.body.ids.length) { // //figure out which is missing to tell user // } const [notes, pinnedEventIds] = await Promise.all([ - Promise.all( - request.body.ids.map(timelineId => getNotesByTimelineId(savedObjectsClient, timelineId)) - ), - Promise.all( - request.body.ids.map(timelineId => - getPinnedEventsByTimelineId(savedObjectsClient, timelineId) - ) - ), + Promise.all(ids.map(timelineId => getNotesByTimelineId(savedObjectsClient, timelineId))), + Promise.all(ids.map(timelineId => getPinnedEventsByTimelineId(savedObjectsClient, timelineId))), ]); const myNotes = notes.reduce( @@ -171,7 +164,7 @@ const getTimelinesFromObjects = async ( [] ); - const myResponse = request.body.ids.reduce((acc, timelineId) => { + const myResponse = ids.reduce((acc, timelineId) => { const myTimeline = timelines.find(t => t.savedObjectId === timelineId); if (myTimeline != null) { const timelineNotes = myNotes.filter(n => n.timelineId === timelineId); @@ -193,11 +186,11 @@ const getTimelinesFromObjects = async ( export const getExportTimelineByObjectIds = async ({ client, - request, + ids, }: { client: ExportTimelineSavedObjectsClient; - request: ExportTimelineRequest; + ids: string[]; }) => { - const timeline = await getTimelinesFromObjects(client, request); + const timeline = await getTimelinesFromObjects(client, ids); return transformDataToNdjson(timeline); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts index 35bf86c17db7e..523221192eca4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts @@ -14,7 +14,7 @@ import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject, } from '../pinned_event/types'; -import { SavedObjectsClient, KibanaRequest } from '../../../../../../../src/core/server'; +import { SavedObjectsClient } from '../../../../../../../src/core/server'; /* * ColumnHeader Types @@ -204,20 +204,9 @@ export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ export interface AllTimelineSavedObject extends runtimeTypes.TypeOf {} -export interface ExportTimelineRequestParams { - body: { ids: string[] }; - query: { - file_name: string; - exclude_export_details: boolean; - }; -} - -export type ExportTimelineRequest = KibanaRequest< - unknown, - ExportTimelineRequestParams['query'], - ExportTimelineRequestParams['body'], - 'post' ->; +/** + * Import/export timelines + */ export type ExportTimelineSavedObjectsClient = Pick< SavedObjectsClient, diff --git a/x-pack/legacy/plugins/siem/server/utils/build_validation/route_validation.test.ts b/x-pack/legacy/plugins/siem/server/utils/build_validation/route_validation.test.ts new file mode 100644 index 0000000000000..888cd5dfe5390 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/utils/build_validation/route_validation.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildRouteValidation } from './route_validation'; +import * as rt from 'io-ts'; +import { RouteValidationResultFactory } from '../../../../../../../src/core/server/http'; + +describe('buildRouteValidation', () => { + const schema = rt.type({ + ids: rt.array(rt.string), + }); + const validationResult: RouteValidationResultFactory = { + ok: jest.fn().mockImplementation(validatedInput => validatedInput), + badRequest: jest.fn().mockImplementation(e => e), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('return validation error', () => { + const input = { id: 'someId' }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual( + 'Invalid value undefined supplied to : { ids: Array }/ids: Array' + ); + }); + + test('return validated input', () => { + const input = { ids: ['someId'] }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual(input); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/utils/build_validation/route_validation.ts b/x-pack/legacy/plugins/siem/server/utils/build_validation/route_validation.ts new file mode 100644 index 0000000000000..1281c23cbc89a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/utils/build_validation/route_validation.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; +import { + RouteValidationFunction, + RouteValidationResultFactory, + RouteValidationError, +} from '../../../../../../../src/core/server'; + +type RequestValidationResult = + | { + value: T; + error?: undefined; + } + | { + value?: undefined; + error: RouteValidationError; + }; + +export const buildRouteValidation = >( + schema: T +): RouteValidationFunction => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +) => + pipe( + schema.decode(inputValue), + fold>( + (errors: rt.Errors) => validationResult.badRequest(failure(errors).join('\n')), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/legacy/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.test.ts new file mode 100644 index 0000000000000..2b5b34edca140 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { transformDataToNdjson } from './create_stream_from_ndjson'; +import { ImportRuleAlertRest } from '../../lib/detection_engine/types'; +import { sampleRule } from '../../lib/detection_engine/signals/__mocks__/es_results'; + +export const getOutputSample = (): Partial => ({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', +}); + +export const getSampleAsNdjson = (sample: Partial): string => { + return `${JSON.stringify(sample)}\n`; +}; + +describe('create_rules_stream_from_ndjson', () => { + describe('transformDataToNdjson', () => { + test('if rules are empty it returns an empty string', () => { + const ruleNdjson = transformDataToNdjson([]); + expect(ruleNdjson).toEqual(''); + }); + + test('single rule will transform with new line ending character for ndjson', () => { + const rule = sampleRule(); + const ruleNdjson = transformDataToNdjson([rule]); + expect(ruleNdjson.endsWith('\n')).toBe(true); + }); + + test('multiple rules will transform with two new line ending characters for ndjson', () => { + const result1 = sampleRule(); + const result2 = sampleRule(); + result2.id = 'some other id'; + result2.rule_id = 'some other id'; + result2.name = 'Some other rule'; + + const ruleNdjson = transformDataToNdjson([result1, result2]); + // this is how we count characters in JavaScript :-) + const count = ruleNdjson.split('\n').length - 1; + expect(count).toBe(2); + }); + + test('you can parse two rules back out without errors', () => { + const result1 = sampleRule(); + const result2 = sampleRule(); + result2.id = 'some other id'; + result2.rule_id = 'some other id'; + result2.name = 'Some other rule'; + + const ruleNdjson = transformDataToNdjson([result1, result2]); + const ruleStrings = ruleNdjson.split('\n'); + const reParsed1 = JSON.parse(ruleStrings[0]); + const reParsed2 = JSON.parse(ruleStrings[1]); + expect(reParsed1).toEqual(result1); + expect(reParsed2).toEqual(result2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.ts new file mode 100644 index 0000000000000..0b7966926b5dd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Transform } from 'stream'; +import { has, isString } from 'lodash/fp'; +import { ImportRuleAlertRest } from '../../lib/detection_engine/types'; +import { createMapStream, createFilterStream } from '../../../../../../../src/legacy/utils/streams'; +import { importRulesSchema } from '../../lib/detection_engine/routes/schemas/import_rules_schema'; +import { BadRequestError } from '../../lib/detection_engine/errors/bad_request_error'; + +export interface RulesObjectsExportResultDetails { + /** number of successfully exported objects */ + exportedCount: number; +} + +export const parseNdjsonStrings = (): Transform => { + return createMapStream((ndJsonStr: string) => { + if (isString(ndJsonStr) && ndJsonStr.trim() !== '') { + try { + return JSON.parse(ndJsonStr); + } catch (err) { + return err; + } + } + }); +}; + +export const filterExportedCounts = (): Transform => { + return createFilterStream( + obj => obj != null && !has('exported_count', obj) + ); +}; + +export const validateRules = (): Transform => { + return createMapStream((obj: ImportRuleAlertRest) => { + if (!(obj instanceof Error)) { + const validated = importRulesSchema.validate(obj); + if (validated.error != null) { + return new BadRequestError(validated.error.message); + } else { + return validated.value; + } + } else { + return obj; + } + }); +}; + +// Adaptation from: saved_objects/import/create_limit_stream.ts +export const createLimitStream = (limit: number): Transform => { + let counter = 0; + return new Transform({ + objectMode: true, + async transform(obj, _, done) { + if (counter >= limit) { + return done(new Error(`Can't import more than ${limit} rules`)); + } + counter++; + done(undefined, obj); + }, + }); +}; + +export const transformDataToNdjson = (data: unknown[]): string => { + if (data.length !== 0) { + const dataString = data.map(rule => JSON.stringify(rule)).join('\n'); + return `${dataString}\n`; + } else { + return ''; + } +}; diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 68c3967359ff4..6bdd30848e4b7 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -23,6 +23,7 @@ describe('execute()', () => { actionTypeRegistry: actionTypeRegistryMock.create(), getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), isESOUsingEphemeralEncryptionKey: false, + preconfiguredActions: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -68,6 +69,68 @@ describe('execute()', () => { }); }); + test('schedules the action with all given parameters with a preconfigured action', async () => { + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), + isESOUsingEphemeralEncryptionKey: false, + preconfiguredActions: [ + { + id: '123', + actionTypeId: 'mock-action-preconfigured', + config: {}, + isPreconfigured: true, + name: 'x', + secrets: {}, + }, + ], + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + await executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: Buffer.from('123:abc').toString('base64'), + }); + expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:mock-action-preconfigured", + }, + ] + `); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalledWith('action_task_params', { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + }); + }); + test('uses API key when provided', async () => { const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); const executeFn = createExecuteFunction({ @@ -76,6 +139,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -125,6 +189,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -171,6 +236,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: true, actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], }); await expect( executeFn({ @@ -193,6 +259,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: mockedActionTypeRegistry, + preconfiguredActions: [], }); mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { throw new Error('Fail'); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 4bbcda4cba7fc..4a9ddf412b7cc 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -6,7 +6,12 @@ import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; -import { GetBasePathFunction, RawAction, ActionTypeRegistryContract } from './types'; +import { + GetBasePathFunction, + RawAction, + ActionTypeRegistryContract, + PreConfiguredAction, +} from './types'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -14,6 +19,7 @@ interface CreateExecuteFunctionOptions { getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; actionTypeRegistry: ActionTypeRegistryContract; + preconfiguredActions: PreConfiguredAction[]; } export interface ExecuteOptions { @@ -29,6 +35,7 @@ export function createExecuteFunction({ actionTypeRegistry, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey, + preconfiguredActions, }: CreateExecuteFunctionOptions) { return async function execute({ id, params, spaceId, apiKey }: ExecuteOptions) { if (isESOUsingEphemeralEncryptionKey === true) { @@ -61,9 +68,9 @@ export function createExecuteFunction({ }; const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); - const actionSavedObject = await savedObjectsClient.get('action', id); + const actionTypeId = await getActionTypeId(id); - actionTypeRegistry.ensureActionTypeEnabled(actionSavedObject.attributes.actionTypeId); + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, @@ -72,7 +79,7 @@ export function createExecuteFunction({ }); await taskManager.schedule({ - taskType: `actions:${actionSavedObject.attributes.actionTypeId}`, + taskType: `actions:${actionTypeId}`, params: { spaceId, actionTaskParamsId: actionTaskParamsRecord.id, @@ -80,5 +87,15 @@ export function createExecuteFunction({ state: {}, scope: ['actions'], }); + + async function getActionTypeId(actionId: string): Promise { + const pcAction = preconfiguredActions.find(action => action.id === actionId); + if (pcAction) { + return pcAction.actionTypeId; + } + + const actionSO = await savedObjectsClient.get('action', actionId); + return actionSO.attributes.actionTypeId; + } }; } diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index bbcb0457fc1d1..124e5951c714b 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -43,6 +43,7 @@ actionExecutor.initialize({ actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), + preconfiguredActions: [], }); beforeEach(() => { @@ -232,6 +233,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), + preconfiguredActions: [], }); await expect( customActionExecutor.execute(executeParams) diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index ba8bb79170c63..a33fb8830a930 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -11,6 +11,8 @@ import { ActionTypeRegistryContract, GetServicesFunction, RawAction, + PreConfiguredAction, + Services, } from '../types'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; @@ -24,6 +26,7 @@ export interface ActionExecutorContext { encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; + preconfiguredActions: PreConfiguredAction[]; } export interface ExecuteOptions { @@ -72,28 +75,22 @@ export class ActionExecutor { encryptedSavedObjectsPlugin, actionTypeRegistry, eventLogger, + preconfiguredActions, } = this.actionExecutorContext!; const services = getServices(request); const spaceId = spaces && spaces.getSpaceId(request); const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; - // Ensure user can read the action before processing - const { - attributes: { actionTypeId, config, name }, - } = await services.savedObjectsClient.get('action', actionId); - - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - - // Only get encrypted attributes here, the remaining attributes can be fetched in - // the savedObjectsClient call - const { - attributes: { secrets }, - } = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser( - 'action', + const { actionTypeId, name, config, secrets } = await getActionInfo( + services, + encryptedSavedObjectsPlugin, + preconfiguredActions, actionId, - namespace + namespace.namespace ); + + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); const actionType = actionTypeRegistry.get(actionTypeId); let validatedParams: Record; @@ -173,3 +170,50 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string { return message; } + +interface ActionInfo { + actionTypeId: string; + name: string; + config: any; + secrets: any; +} + +async function getActionInfo( + services: Services, + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart, + preconfiguredActions: PreConfiguredAction[], + actionId: string, + namespace: string | undefined +): Promise { + // check to see if it's a pre-configured action first + const pcAction = preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === actionId + ); + if (pcAction) { + return { + actionTypeId: pcAction.actionTypeId, + name: pcAction.name, + config: pcAction.config, + secrets: pcAction.secrets, + }; + } + + // if not pre-configured action, should be a saved object + // ensure user can read the action before processing + const { + attributes: { actionTypeId, config, name }, + } = await services.savedObjectsClient.get('action', actionId); + + const { + attributes: { secrets }, + } = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', actionId, { + namespace: namespace === 'default' ? undefined : namespace, + }); + + return { + actionTypeId, + name, + config, + secrets, + }; +} diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 43882cef21170..f070f714ee508 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -61,6 +61,7 @@ const actionExecutorInitializerParams = { actionTypeRegistry, encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), + preconfiguredActions: [], }; const taskRunnerFactoryInitializerParams = { spaceIdToNamespace, diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 6215b08df81d4..fa5b2f9399a4d 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -99,6 +99,9 @@ describe('Actions Plugin', () => { savedObjects: { client: {}, }, + elasticsearch: { + adminClient: jest.fn(), + }, }, } as any, httpServerMock.createKibanaRequest(), diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 34c9e7aa9e8b8..a8ab3bbb2fad2 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -11,13 +11,13 @@ import { Plugin, CoreSetup, CoreStart, - IClusterClient, KibanaRequest, Logger, SharedGlobalConfig, RequestHandler, IContextProvider, SavedObjectsServiceStart, + ElasticsearchServiceStart, } from '../../../../src/core/server'; import { @@ -89,7 +89,6 @@ export class ActionsPlugin implements Plugin, Plugi private readonly logger: Logger; private serverBasePath?: string; - private adminClient?: IClusterClient; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; @@ -173,7 +172,6 @@ export class ActionsPlugin implements Plugin, Plugi this.actionTypeRegistry = actionTypeRegistry; this.serverBasePath = core.http.basePath.serverBasePath; this.actionExecutor = actionExecutor; - this.adminClient = core.elasticsearch.adminClient; this.spaces = plugins.spaces?.spacesService; registerBuiltInActionTypes({ @@ -233,7 +231,6 @@ export class ActionsPlugin implements Plugin, Plugi actionTypeRegistry, taskRunnerFactory, kibanaIndex, - adminClient, isESOUsingEphemeralEncryptionKey, preconfiguredActions, } = this; @@ -242,9 +239,10 @@ export class ActionsPlugin implements Plugin, Plugi logger, eventLogger: this.eventLogger!, spaces: this.spaces, - getServices: this.getServicesFactory(core.savedObjects), + getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, actionTypeRegistry: actionTypeRegistry!, + preconfiguredActions, }); taskRunnerFactory!.initialize({ @@ -265,6 +263,7 @@ export class ActionsPlugin implements Plugin, Plugi getScopedSavedObjectsClient: core.savedObjects.getScopedClient, getBasePath: this.getBasePath, isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, + preconfiguredActions, }), isActionTypeEnabled: id => { return this.actionTypeRegistry!.isActionTypeEnabled(id); @@ -280,7 +279,7 @@ export class ActionsPlugin implements Plugin, Plugi savedObjectsClient: core.savedObjects.getScopedClient(request), actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex: await kibanaIndex, - scopedClusterClient: adminClient!.asScoped(request), + scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), preconfiguredActions, }); }, @@ -289,11 +288,11 @@ export class ActionsPlugin implements Plugin, Plugi } private getServicesFactory( - savedObjects: SavedObjectsServiceStart + savedObjects: SavedObjectsServiceStart, + elasticsearch: ElasticsearchServiceStart ): (request: KibanaRequest) => Services { - const { adminClient } = this; return request => ({ - callCluster: adminClient!.asScoped(request).callAsCurrentUser, + callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), }); } @@ -301,12 +300,8 @@ export class ActionsPlugin implements Plugin, Plugi private createRouteHandlerContext = ( defaultKibanaIndex: string ): IContextProvider, 'actions'> => { - const { - actionTypeRegistry, - adminClient, - isESOUsingEphemeralEncryptionKey, - preconfiguredActions, - } = this; + const { actionTypeRegistry, isESOUsingEphemeralEncryptionKey, preconfiguredActions } = this; + return async function actionsRouteHandlerContext(context, request) { return { getActionsClient: () => { @@ -319,7 +314,7 @@ export class ActionsPlugin implements Plugin, Plugi savedObjectsClient: context.core.savedObjects.client, actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex, - scopedClusterClient: adminClient!.asScoped(request), + scopedClusterClient: context.core.elasticsearch.adminClient, preconfiguredActions, }); }, diff --git a/x-pack/plugins/actions/server/usage/task.ts b/x-pack/plugins/actions/server/usage/task.ts index a07a2aa8f1c70..ed0d876ed0208 100644 --- a/x-pack/plugins/actions/server/usage/task.ts +++ b/x-pack/plugins/actions/server/usage/task.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, CoreSetup } from 'kibana/server'; +import { Logger, CoreSetup, APICaller } from 'kibana/server'; import moment from 'moment'; import { RunContext, @@ -62,7 +62,11 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export function telemetryTaskRunner(logger: Logger, core: CoreSetup, kibanaIndex: string) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = core.elasticsearch.adminClient.callAsInternalUser; + const callCluster = (...args: Parameters) => { + return core.getStartServices().then(([{ elasticsearch: { legacy: { client } } }]) => + client.callAsInternalUser(...args) + ); + }; return { async run() { return Promise.all([ diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 6f8478df58a53..3e4c26d3444c9 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -34,6 +34,7 @@ import { import { EncryptedSavedObjectsPluginStart } from '../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; +import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = @@ -268,7 +269,7 @@ export class AlertsClient { const removeResult = await this.savedObjectsClient.delete('alert', id); await Promise.all([ - taskIdToRemove ? this.taskManager.remove(taskIdToRemove) : null, + taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, ]); @@ -510,7 +511,9 @@ export class AlertsClient { ); await Promise.all([ - attributes.scheduledTaskId ? this.taskManager.remove(attributes.scheduledTaskId) : null, + attributes.scheduledTaskId + ? deleteTaskIfItExists(this.taskManager, attributes.scheduledTaskId) + : null, apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, ]); } diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts new file mode 100644 index 0000000000000..84a1743387c9c --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { taskManagerMock } from '../../../task_manager/server/mocks'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { deleteTaskIfItExists } from './delete_task_if_it_exists'; + +describe('deleteTaskIfItExists', () => { + test('removes the task by its ID', async () => { + const tm = taskManagerMock.createStart(); + const id = uuid.v4(); + + expect(await deleteTaskIfItExists(tm, id)).toBe(undefined); + + expect(tm.remove).toHaveBeenCalledWith(id); + }); + + test('handles 404 errors caused by the task not existing', async () => { + const tm = taskManagerMock.createStart(); + const id = uuid.v4(); + + tm.remove.mockRejectedValue(SavedObjectsErrorHelpers.createGenericNotFoundError('task', id)); + + expect(await deleteTaskIfItExists(tm, id)).toBe(undefined); + + expect(tm.remove).toHaveBeenCalledWith(id); + }); + + test('throws if any other errro is caused by task removal', async () => { + const tm = taskManagerMock.createStart(); + const id = uuid.v4(); + + const error = SavedObjectsErrorHelpers.createInvalidVersionError(uuid.v4()); + tm.remove.mockRejectedValue(error); + + expect(deleteTaskIfItExists(tm, id)).rejects.toBe(error); + + expect(tm.remove).toHaveBeenCalledWith(id); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts new file mode 100644 index 0000000000000..53bb1b5cb5d53 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.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 { TaskManagerStartContract } from '../../../task_manager/server'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +export async function deleteTaskIfItExists(taskManager: TaskManagerStartContract, taskId: string) { + try { + await taskManager.remove(taskId); + } catch (err) { + if (!SavedObjectsErrorHelpers.isNotFoundError(err)) { + throw err; + } + } +} diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts new file mode 100644 index 0000000000000..46ceee3ce420b --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.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 { isAlertSavedObjectNotFoundError } from './is_alert_not_found_error'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import uuid from 'uuid'; + +describe('isAlertSavedObjectNotFoundError', () => { + test('identifies SavedObjects Not Found errors', () => { + const id = uuid.v4(); + // ensure the error created by SO parses as a string with the format we expect + expect( + `${SavedObjectsErrorHelpers.createGenericNotFoundError('alert', id)}`.includes(`alert/${id}`) + ).toBe(true); + + const errorBySavedObjectsHelper = SavedObjectsErrorHelpers.createGenericNotFoundError( + 'alert', + id + ); + + expect(isAlertSavedObjectNotFoundError(errorBySavedObjectsHelper, id)).toBe(true); + }); + + test('identifies generic errors', () => { + const id = uuid.v4(); + expect(isAlertSavedObjectNotFoundError(new Error(`not found`), id)).toBe(false); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts new file mode 100644 index 0000000000000..0aa83ad0e883c --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.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 { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +export function isAlertSavedObjectNotFoundError(err: Error, alertId: string) { + return SavedObjectsErrorHelpers.isNotFoundError(err) && `${err}`.includes(alertId); +} diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index c5a99e1c95793..e930e1f2e5456 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -19,7 +19,6 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertsClientFactory } from './alerts_client_factory'; import { LicenseState } from './lib/license_state'; import { - IClusterClient, KibanaRequest, Logger, PluginInitializerContext, @@ -29,6 +28,7 @@ import { IContextProvider, RequestHandler, SharedGlobalConfig, + ElasticsearchServiceStart, } from '../../../../src/core/server'; import { @@ -96,7 +96,6 @@ export class AlertingPlugin { private readonly logger: Logger; private alertTypeRegistry?: AlertTypeRegistry; private readonly taskRunnerFactory: TaskRunnerFactory; - private adminClient?: IClusterClient; private serverBasePath?: string; private licenseState: LicenseState | null = null; private isESOUsingEphemeralEncryptionKey?: boolean; @@ -122,7 +121,6 @@ export class AlertingPlugin { } public async setup(core: CoreSetup, plugins: AlertingPluginsSetup): Promise { - this.adminClient = core.elasticsearch.adminClient; this.licenseState = new LicenseState(plugins.licensing.license$); this.spaces = plugins.spaces?.spacesService; this.security = plugins.security; @@ -227,7 +225,7 @@ export class AlertingPlugin { taskRunnerFactory.initialize({ logger, - getServices: this.getServicesFactory(core.savedObjects), + getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), spaceIdToNamespace: this.spaceIdToNamespace, actionsPlugin: plugins.actions, encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, @@ -267,7 +265,8 @@ export class AlertingPlugin { }; private getServicesFactory( - savedObjects: SavedObjectsServiceStart + savedObjects: SavedObjectsServiceStart, + elasticsearch: ElasticsearchServiceStart ): (request: KibanaRequest) => Services { const { adminClient } = this; return request => { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 4e6d959f0ce60..31cc893f785cb 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -16,6 +16,7 @@ import { PluginStartContract as ActionsPluginStart } from '../../../actions/serv import { actionsMock } from '../../../actions/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; const alertType = { id: 'test', @@ -665,4 +666,36 @@ describe('Task Runner', () => { } `); }); + + test('avoids rescheduling a failed Alert Task Runner when it throws due to failing to fetch the alert', async () => { + savedObjectsClient.get.mockImplementation(() => { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', '1'); + }); + + const taskRunner = new TaskRunner( + alertType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + + encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const runnerResult = await taskRunner.run(); + + expect(runnerResult).toMatchInlineSnapshot(` + Object { + "runAt": undefined, + "state": Object { + "previousStartedAt": 1970-01-01T00:00:00.000Z, + }, + } + `); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 190cdc184930c..1d4b12e96bc76 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -26,12 +26,13 @@ import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; import { AlertInstances } from '../alert_instance/alert_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger } from '../../../event_log/server'; +import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; interface AlertTaskRunResult { state: AlertTaskState; - runAt: Date; + runAt: Date | undefined; } interface AlertTaskInstance extends ConcreteTaskInstance { @@ -328,22 +329,29 @@ export class TaskRunner { }; }, (err: Error) => { - this.logger.error(`Executing Alert "${alertId}" has resulted in Error: ${err.message}`); + const message = `Executing Alert "${alertId}" has resulted in Error: ${err.message}`; + if (isAlertSavedObjectNotFoundError(err, alertId)) { + this.logger.debug(message); + } else { + this.logger.error(message); + } return { ...originalState, previousStartedAt, }; } ), - runAt: resolveErr(runAt, () => - getNextRunAt( - new Date(), - // if we fail at this point we wish to recover but don't have access to the Alert's - // attributes, so we'll use a default interval to prevent the underlying task from - // falling into a failed state - FALLBACK_RETRY_INTERVAL - ) - ), + runAt: resolveErr(runAt, err => { + return isAlertSavedObjectNotFoundError(err, alertId) + ? undefined + : getNextRunAt( + new Date(), + // if we fail at this point we wish to recover but don't have access to the Alert's + // attributes, so we'll use a default interval to prevent the underlying task from + // falling into a failed state + FALLBACK_RETRY_INTERVAL + ); + }), }; } } diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerting/server/usage/task.ts index 3da60aef301e2..ab62d81d44f8a 100644 --- a/x-pack/plugins/alerting/server/usage/task.ts +++ b/x-pack/plugins/alerting/server/usage/task.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, CoreSetup } from 'kibana/server'; +import { Logger, CoreSetup, APICaller } from 'kibana/server'; import moment from 'moment'; import { RunContext, @@ -65,7 +65,12 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export function telemetryTaskRunner(logger: Logger, core: CoreSetup, kibanaIndex: string) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = core.elasticsearch.adminClient.callAsInternalUser; + const callCluster = (...args: Parameters) => { + return core.getStartServices().then(([{ elasticsearch: { legacy: { client } } }]) => + client.callAsInternalUser(...args) + ); + }; + return { async run() { return Promise.all([ diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e18b6d33ca419..b434d41982f4c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -3,7 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + CoreStart, + Logger +} from 'src/core/server'; import { Observable, combineLatest, AsyncSubject } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { Server } from 'hapi'; @@ -37,6 +43,8 @@ export interface APMPluginContract { } export class APMPlugin implements Plugin { + private currentConfig?: APMConfig; + private logger?: Logger; legacySetup$: AsyncSubject; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; @@ -56,7 +64,7 @@ export class APMPlugin implements Plugin { actions?: ActionsPlugin['setup']; } ) { - const logger = this.initContext.logger.get(); + this.logger = this.initContext.logger.get(); const config$ = this.initContext.config.create(); const mergedConfig$ = combineLatest(plugins.apm_oss.config$, config$).pipe( map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig)) @@ -71,49 +79,40 @@ export class APMPlugin implements Plugin { } this.legacySetup$.subscribe(__LEGACY => { - createApmApi().init(core, { config$: mergedConfig$, logger, __LEGACY }); + createApmApi().init(core, { + config$: mergedConfig$, + logger: this.logger!, + __LEGACY + }); }); - const currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); if ( plugins.taskManager && plugins.usageCollection && - currentConfig['xpack.apm.telemetryCollectionEnabled'] + this.currentConfig['xpack.apm.telemetryCollectionEnabled'] ) { createApmTelemetry({ core, config$: mergedConfig$, usageCollector: plugins.usageCollection, taskManager: plugins.taskManager, - logger + logger: this.logger }); } - // create agent configuration index without blocking setup lifecycle - createApmAgentConfigurationIndex({ - esClient: core.elasticsearch.dataClient, - config: currentConfig, - logger - }); - // create custom action index without blocking setup lifecycle - createApmCustomLinkIndex({ - esClient: core.elasticsearch.dataClient, - config: currentConfig, - logger - }); - plugins.home.tutorials.registerTutorial( tutorialProvider({ - isEnabled: currentConfig['xpack.apm.ui.enabled'], - indexPatternTitle: currentConfig['apm_oss.indexPattern'], + isEnabled: this.currentConfig['xpack.apm.ui.enabled'], + indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], cloud: plugins.cloud, indices: { - errorIndices: currentConfig['apm_oss.errorIndices'], - metricsIndices: currentConfig['apm_oss.metricsIndices'], - onboardingIndices: currentConfig['apm_oss.onboardingIndices'], - sourcemapIndices: currentConfig['apm_oss.sourcemapIndices'], - transactionIndices: currentConfig['apm_oss.transactionIndices'] + errorIndices: this.currentConfig['apm_oss.errorIndices'], + metricsIndices: this.currentConfig['apm_oss.metricsIndices'], + onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], + sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], + transactionIndices: this.currentConfig['apm_oss.transactionIndices'] } }) ); @@ -127,12 +126,29 @@ export class APMPlugin implements Plugin { getApmIndices: async () => getApmIndices({ savedObjectsClient: await getInternalSavedObjectsClient(core), - config: currentConfig + config: await mergedConfig$.pipe(take(1)).toPromise() }) }; } - public async start() {} + public start(core: CoreStart) { + if (this.currentConfig == null || this.logger == null) { + throw new Error('APMPlugin needs to be setup before calling start()'); + } + + // create agent configuration index without blocking start lifecycle + createApmAgentConfigurationIndex({ + esClient: core.elasticsearch.legacy.client, + config: this.currentConfig, + logger: this.logger + }); + // create custom action index without blocking start lifecycle + createApmCustomLinkIndex({ + esClient: core.elasticsearch.legacy.client, + config: this.currentConfig, + logger: this.logger + }); + } public stop() {} } diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 3c5d3405f395e..1f08a41024905 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -167,6 +167,7 @@ export type CasesResponse = rt.TypeOf; export type CasesFindResponse = rt.TypeOf; export type CasePatchRequest = rt.TypeOf; export type CasesPatchRequest = rt.TypeOf; +export type Status = rt.TypeOf; export type CaseExternalServiceRequest = rt.TypeOf; export type ServiceConnectorCaseParams = rt.TypeOf; export type ServiceConnectorCaseResponse = rt.TypeOf; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index 95cd66a9c51a2..e83dafc68ee69 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -11,14 +11,20 @@ import { SavedObjectsBulkUpdateObject, } from 'src/core/server'; -import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../saved_object_types'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_SAVED_OBJECT, + CASE_CONFIGURE_SAVED_OBJECT, +} from '../../../saved_object_types'; export const createMockSavedObjectsRepository = ({ caseSavedObject = [], caseCommentSavedObject = [], + caseConfigureSavedObject = [], }: { caseSavedObject?: any[]; caseCommentSavedObject?: any[]; + caseConfigureSavedObject?: any[]; }) => { const mockSavedObjectsClientContract = ({ bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => { @@ -70,6 +76,7 @@ export const createMockSavedObjectsRepository = ({ } return result[0]; } + const result = caseSavedObject.filter(s => s.id === id); if (!result.length) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); @@ -81,6 +88,23 @@ export const createMockSavedObjectsRepository = ({ throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } + if ( + findArgs.type === CASE_CONFIGURE_SAVED_OBJECT && + caseConfigureSavedObject[0] && + caseConfigureSavedObject[0].id === 'throw-error-find' + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('Error thrown for testing'); + } + + if (findArgs.type === CASE_CONFIGURE_SAVED_OBJECT) { + return { + page: 1, + per_page: 5, + total: caseConfigureSavedObject.length, + saved_objects: caseConfigureSavedObject, + }; + } + if (findArgs.type === CASE_COMMENT_SAVED_OBJECT) { return { page: 1, @@ -101,6 +125,13 @@ export const createMockSavedObjectsRepository = ({ throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } + if ( + type === CASE_CONFIGURE_SAVED_OBJECT && + attributes.connector_id === 'throw-error-create' + ) { + throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); + } + if (type === CASE_COMMENT_SAVED_OBJECT) { const newCommentObj = { type, @@ -113,6 +144,20 @@ export const createMockSavedObjectsRepository = ({ caseCommentSavedObject = [...caseCommentSavedObject, newCommentObj]; return newCommentObj; } + + if (type === CASE_CONFIGURE_SAVED_OBJECT) { + const newConfiguration = { + type, + id: 'mock-configuration', + attributes, + updated_at: '2020-04-09T09:43:51.778Z', + version: attributes.connector_id === 'no-version' ? undefined : 'WzksMV0=', + }; + + caseConfigureSavedObject = [newConfiguration]; + return newConfiguration; + } + return { type, id: 'mock-it', @@ -143,6 +188,16 @@ export const createMockSavedObjectsRepository = ({ } } + if (type === CASE_CONFIGURE_SAVED_OBJECT) { + return { + id, + type, + updated_at: '2019-11-22T22:50:55.191Z', + attributes, + version: attributes.connector_id === 'no-version' ? undefined : 'WzE3LDFd', + }; + } + return { id, type, @@ -153,16 +208,29 @@ export const createMockSavedObjectsRepository = ({ }), delete: jest.fn((type: string, id: string) => { let result = caseSavedObject.filter(s => s.id === id); + if (type === CASE_COMMENT_SAVED_OBJECT) { result = caseCommentSavedObject.filter(s => s.id === id); } + + if (type === CASE_CONFIGURE_SAVED_OBJECT) { + result = caseConfigureSavedObject.filter(s => s.id === id); + } + if (type === CASE_COMMENT_SAVED_OBJECT && id === 'bad-guy') { throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } + if (!result.length) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } + if ( + type === CASE_CONFIGURE_SAVED_OBJECT && + caseConfigureSavedObject[0].id === 'throw-error-delete' + ) { + throw new Error('Error thrown for testing'); + } return {}; }), deleteByNamespace: jest.fn(), diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 03da50f886fd5..75e793a80272f 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -5,7 +5,11 @@ */ import { SavedObject } from 'kibana/server'; -import { CaseAttributes, CommentAttributes } from '../../../../common/api'; +import { + CaseAttributes, + CommentAttributes, + CasesConfigureAttributes, +} from '../../../../common/api'; export const mockCases: Array> = [ { @@ -225,7 +229,33 @@ export const mockCaseComments: Array> = [ }, ], updated_at: '2019-11-25T22:32:30.608Z', + version: 'WzYsMV0=', + }, +]; +export const mockCaseConfigure: Array> = [ + { + type: 'cases-configure', + id: 'mock-configuration-1', + attributes: { + connector_id: '123', + connector_name: 'My connector', + closure_type: 'close-by-user', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + references: [], + updated_at: '2020-04-09T09:43:51.778Z', version: 'WzYsMV0=', }, ]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index b1881e394e796..d947ffbaf181d 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -5,13 +5,19 @@ */ import { RequestHandlerContext } from 'src/core/server'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { getActions } from '../__mocks__/request_responses'; export const createRouteContext = (client: any) => { + const actionsMock = actionsClientMock.create(); + actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + return ({ core: { savedObjects: { client, }, }, + actions: { getActionsClient: () => actionsMock }, } as unknown) as RequestHandlerContext; }; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts new file mode 100644 index 0000000000000..846013674986e --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CasePostRequest, CasesConfigureRequest } from '../../../../common/api'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FindActionResult } from '../../../../../actions/server/types'; + +export const newCase: CasePostRequest = { + title: 'My new case', + description: 'A description', + tags: ['new', 'case'], +}; + +export const getActions = (): FindActionResult[] => [ + { + id: 'e90075a5-c386-41e3-ae21-ba4e61510695', + actionTypeId: '.webhook', + name: 'Test', + config: { + method: 'post', + url: 'https://example.com', + headers: null, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: 'd611af27-3532-4da9-8034-271fee81d634', + actionTypeId: '.servicenow', + name: 'ServiceNow', + config: { + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://dev102283.service-now.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, +]; + +export const newConfiguration: CasesConfigureRequest = { + connector_id: '456', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', +}; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts new file mode 100644 index 0000000000000..66d39c3f11d28 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initGetCaseConfigure } from './get_configure'; + +describe('GET configuration', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initGetCaseConfigure, 'get'); + }); + + it('returns the configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({ + ...mockCaseConfigure[0].attributes, + version: mockCaseConfigure[0].version, + }); + }); + + it('handles undefined version correctly', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({ + connector_id: '123', + connector_name: 'My connector', + closure_type: 'close-by-user', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: '', + }); + }); + + it('returns an empty object when there is no configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({}); + }); + + it('returns an error if find throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(404); + expect(res.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts new file mode 100644 index 0000000000000..62edaa0a4792a --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initCaseConfigureGetActionConnector } from './get_connectors'; +import { getActions } from '../../__mocks__/request_responses'; + +describe('GET connectors', () => { + let routeHandler: RequestHandler; + beforeAll(async () => { + routeHandler = await createRoute(initCaseConfigureGetActionConnector, 'get'); + }); + + it('returns the connectors', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure/connectors/_find', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + getActions().filter(action => action.actionTypeId === '.servicenow') + ); + }); + + it('it throws an error when actions client is null', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure/connectors/_find', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + context.actions = undefined; + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(404); + expect(res.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts new file mode 100644 index 0000000000000..5b3d68a258664 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initPatchCaseConfigure } from './patch_configure'; + +describe('PATCH configuration', () => { + let routeHandler: RequestHandler; + + beforeAll(async () => { + routeHandler = await createRoute(initPatchCaseConfigure, 'patch'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), + })); + }); + + it('patch configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: mockCaseConfigure[0].version, + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + ...mockCaseConfigure[0].attributes, + closure_type: 'close-by-pushing', + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }) + ); + }); + + it('patch configuration without authentication', async () => { + routeHandler = await createRoute(initPatchCaseConfigure, 'patch', true); + + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: mockCaseConfigure[0].version, + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + ...mockCaseConfigure[0].attributes, + closure_type: 'close-by-pushing', + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { email: null, full_name: null, username: null }, + version: 'WzE3LDFd', + }) + ); + }); + + it('throw error when configuration have not being created', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: mockCaseConfigure[0].version, + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(409); + expect(res.payload.isBoom).toEqual(true); + }); + + it('throw error when the versions are different', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: 'different-version', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(409); + expect(res.payload.isBoom).toEqual(true); + }); + + it('handles undefined version correctly', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { connector_id: 'no-version', version: mockCaseConfigure[0].version }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.payload).toEqual( + expect.objectContaining({ + version: '', + }) + ); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts new file mode 100644 index 0000000000000..7e40cad5b1298 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -0,0 +1,294 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initPostCaseConfigure } from './post_configure'; +import { newConfiguration } from '../../__mocks__/request_responses'; + +describe('POST configuration', () => { + let routeHandler: RequestHandler; + + beforeAll(async () => { + routeHandler = await createRoute(initPostCaseConfigure, 'post'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), + })); + }); + + it('create configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + connector_id: '456', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + updated_at: null, + updated_by: null, + }) + ); + }); + + it('create configuration without authentication', async () => { + routeHandler = await createRoute(initPostCaseConfigure, 'post', true); + + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + connector_id: '456', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: null, full_name: null, username: null }, + updated_at: null, + updated_by: null, + }) + ); + }); + + it('throws when missing connector_id', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('throws when missing connector_name', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_id: '456', + closure_type: 'close-by-pushing', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('throws when missing closure_type', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_id: '456', + connector_name: 'My connector 2', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('it deletes the previous configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const savedObjectRepository = createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }); + + const context = createRouteContext(savedObjectRepository); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(savedObjectRepository.delete.mock.calls[0][1]).toBe(mockCaseConfigure[0].id); + }); + + it('it does NOT delete when not found', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const savedObjectRepository = createMockSavedObjectsRepository({ + caseConfigureSavedObject: [], + }); + + const context = createRouteContext(savedObjectRepository); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(savedObjectRepository.delete).not.toHaveBeenCalled(); + }); + + it('it deletes all configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const savedObjectRepository = createMockSavedObjectsRepository({ + caseConfigureSavedObject: [ + mockCaseConfigure[0], + { ...mockCaseConfigure[0], id: 'mock-configuration-2' }, + ], + }); + + const context = createRouteContext(savedObjectRepository); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(savedObjectRepository.delete.mock.calls[0][1]).toBe(mockCaseConfigure[0].id); + expect(savedObjectRepository.delete.mock.calls[1][1]).toBe('mock-configuration-2'); + }); + + it('returns an error if find throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(404); + expect(res.payload.isBoom).toEqual(true); + }); + + it('returns an error if delete throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(500); + expect(res.payload.isBoom).toEqual(true); + }); + + it('returns an error if post throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_id: 'throw-error-create', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('handles undefined version correctly', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { ...newConfiguration, connector_id: 'no-version' }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + version: '', + }) + ); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts new file mode 100644 index 0000000000000..a22f4db30bf8d --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -0,0 +1,418 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + transformNewCase, + transformNewComment, + wrapError, + transformCases, + flattenCaseSavedObjects, + flattenCaseSavedObject, + flattenCommentSavedObjects, + transformComments, + flattenCommentSavedObject, + sortToSnake, +} from './utils'; +import { newCase } from './__mocks__/request_responses'; +import { isBoom, boomify } from 'boom'; +import { mockCases, mockCaseComments } from './__fixtures__/mock_saved_objects'; + +describe('Utils', () => { + describe('transformNewCase', () => { + it('transform correctly', () => { + const myCase = { + newCase, + createdDate: '2020-04-09T09:43:51.778Z', + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }; + + const res = transformNewCase(myCase); + + expect(res).toEqual({ + ...myCase.newCase, + closed_at: null, + closed_by: null, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' }, + external_service: null, + status: 'open', + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly without optional fields', () => { + const myCase = { + newCase, + createdDate: '2020-04-09T09:43:51.778Z', + }; + + const res = transformNewCase(myCase); + + expect(res).toEqual({ + ...myCase.newCase, + closed_at: null, + closed_by: null, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: undefined, full_name: undefined, username: undefined }, + external_service: null, + status: 'open', + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly with optional fields as null', () => { + const myCase = { + newCase, + createdDate: '2020-04-09T09:43:51.778Z', + email: null, + full_name: null, + username: null, + }; + + const res = transformNewCase(myCase); + + expect(res).toEqual({ + ...myCase.newCase, + closed_at: null, + closed_by: null, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: null, full_name: null, username: null }, + external_service: null, + status: 'open', + updated_at: null, + updated_by: null, + }); + }); + }); + + describe('transformNewComment', () => { + it('transforms correctly', () => { + const comment = { + comment: 'A comment', + createdDate: '2020-04-09T09:43:51.778Z', + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }; + + const res = transformNewComment(comment); + expect(res).toEqual({ + comment: 'A comment', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly without optional fields', () => { + const comment = { + comment: 'A comment', + createdDate: '2020-04-09T09:43:51.778Z', + }; + + const res = transformNewComment(comment); + + expect(res).toEqual({ + comment: 'A comment', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: undefined, full_name: undefined, username: undefined }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly with optional fields as null', () => { + const comment = { + comment: 'A comment', + createdDate: '2020-04-09T09:43:51.778Z', + email: null, + full_name: null, + username: null, + }; + + const res = transformNewComment(comment); + + expect(res).toEqual({ + comment: 'A comment', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: null, full_name: null, username: null }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }); + }); + }); + + describe('wrapError', () => { + it('wraps an error', () => { + const error = new Error('Something happened'); + const res = wrapError(error); + + expect(isBoom(res.body as Error)).toBe(true); + }); + + it('it set statusCode to 500', () => { + const error = new Error('Something happened'); + const res = wrapError(error); + + expect(res.statusCode).toBe(500); + }); + + it('it set statusCode to errors status code', () => { + const error = new Error('Something happened') as any; + error.statusCode = 404; + const res = wrapError(error); + + expect(res.statusCode).toBe(404); + }); + + it('it accepts a boom error', () => { + const error = boomify(new Error('Something happened')); + const res = wrapError(error); + + // Utils returns the same boom error as body + expect(res.body).toBe(error); + }); + + it('it accepts a boom error with status code', () => { + const error = boomify(new Error('Something happened'), { statusCode: 404 }); + const res = wrapError(error); + + expect(res.statusCode).toBe(404); + }); + + it('it returns empty headers', () => { + const error = new Error('Something happened'); + const res = wrapError(error); + + expect(res.headers).toEqual({}); + }); + }); + + describe('transformCases', () => { + it('transforms correctly', () => { + const totalCommentsByCase = [ + { caseId: mockCases[0].id, totalComments: 2 }, + { caseId: mockCases[1].id, totalComments: 2 }, + { caseId: mockCases[2].id, totalComments: 2 }, + { caseId: mockCases[3].id, totalComments: 2 }, + ]; + + const res = transformCases( + { saved_objects: mockCases, total: mockCases.length, per_page: 10, page: 1 }, + 2, + 2, + totalCommentsByCase + ); + expect(res).toEqual({ + page: 1, + per_page: 10, + total: mockCases.length, + cases: flattenCaseSavedObjects(mockCases, totalCommentsByCase), + count_open_cases: 2, + count_closed_cases: 2, + }); + }); + }); + + describe('flattenCaseSavedObjects', () => { + it('flattens correctly', () => { + const totalCommentsByCase = [{ caseId: mockCases[0].id, totalComments: 2 }]; + + const res = flattenCaseSavedObjects([mockCases[0]], totalCommentsByCase); + expect(res).toEqual([ + { + id: 'mock-id-1', + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + comments: [], + totalComment: 2, + version: 'WzAsMV0=', + }, + ]); + }); + + it('it handles total comments correctly', () => { + const totalCommentsByCase = [{ caseId: 'not-exist', totalComments: 2 }]; + + const res = flattenCaseSavedObjects([mockCases[0]], totalCommentsByCase); + + expect(res).toEqual([ + { + id: 'mock-id-1', + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + comments: [], + totalComment: 0, + version: 'WzAsMV0=', + }, + ]); + }); + }); + + describe('flattenCaseSavedObject', () => { + it('flattens correctly', () => { + const myCase = { ...mockCases[0] }; + const res = flattenCaseSavedObject(myCase, [], 2); + expect(res).toEqual({ + id: myCase.id, + version: myCase.version, + comments: [], + totalComment: 2, + ...myCase.attributes, + }); + }); + + it('flattens correctly without version', () => { + const myCase = { ...mockCases[0] }; + myCase.version = undefined; + const res = flattenCaseSavedObject(myCase, [], 2); + expect(res).toEqual({ + id: myCase.id, + version: '0', + comments: [], + totalComment: 2, + ...myCase.attributes, + }); + }); + + it('flattens correctly with comments', () => { + const myCase = { ...mockCases[0] }; + const comments = [{ ...mockCaseComments[0] }]; + const res = flattenCaseSavedObject(myCase, comments, 2); + expect(res).toEqual({ + id: myCase.id, + version: myCase.version, + comments: flattenCommentSavedObjects(comments), + totalComment: 2, + ...myCase.attributes, + }); + }); + }); + + describe('transformComments', () => { + it('transforms correctly', () => { + const comments = { + saved_objects: mockCaseComments, + total: mockCaseComments.length, + per_page: 10, + page: 1, + }; + + const res = transformComments(comments); + expect(res).toEqual({ + page: 1, + per_page: 10, + total: mockCaseComments.length, + comments: flattenCommentSavedObjects(comments.saved_objects), + }); + }); + }); + + describe('flattenCommentSavedObjects', () => { + it('flattens correctly', () => { + const comments = [{ ...mockCaseComments[0] }, { ...mockCaseComments[1] }]; + const res = flattenCommentSavedObjects(comments); + expect(res).toEqual([ + flattenCommentSavedObject(comments[0]), + flattenCommentSavedObject(comments[1]), + ]); + }); + }); + + describe('flattenCommentSavedObject', () => { + it('flattens correctly', () => { + const comment = { ...mockCaseComments[0] }; + const res = flattenCommentSavedObject(comment); + expect(res).toEqual({ + id: comment.id, + version: comment.version, + ...comment.attributes, + }); + }); + + it('flattens correctly without version', () => { + const comment = { ...mockCaseComments[0] }; + comment.version = undefined; + const res = flattenCommentSavedObject(comment); + expect(res).toEqual({ + id: comment.id, + version: '0', + ...comment.attributes, + }); + }); + }); + + describe('sortToSnake', () => { + it('it transforms status correctly', () => { + expect(sortToSnake('status')).toBe('status'); + }); + + it('it transforms createdAt correctly', () => { + expect(sortToSnake('createdAt')).toBe('created_at'); + }); + + it('it transforms created_at correctly', () => { + expect(sortToSnake('created_at')).toBe('created_at'); + }); + + it('it transforms closedAt correctly', () => { + expect(sortToSnake('closedAt')).toBe('closed_at'); + }); + + it('it transforms closed_at correctly', () => { + expect(sortToSnake('closed_at')).toBe('closed_at'); + }); + + it('it transforms default correctly', () => { + expect(sortToSnake('not-exist')).toBe('created_at'); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 3f783d90e577d..1978d780f54f5 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -6,11 +6,8 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; -import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { PolicyData } from '../public/applications/endpoint/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { generatePolicy } from '../public/applications/endpoint/models/policy'; +import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields, PolicyData } from './types'; +import { factory as policyFactory } from './models/policy_config'; export type Event = AlertEvent | EndpointEvent; @@ -474,7 +471,7 @@ export class EndpointDocGenerator { streams: [], config: { policy: { - value: generatePolicy(), + value: policyFactory(), }, }, }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts b/x-pack/plugins/endpoint/common/models/policy_config.ts similarity index 89% rename from x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts rename to x-pack/plugins/endpoint/common/models/policy_config.ts index 5269ee72f4039..199b8a91e4307 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts +++ b/x-pack/plugins/endpoint/common/models/policy_config.ts @@ -7,11 +7,9 @@ import { PolicyConfig, ProtectionModes } from '../types'; /** - * Generate a new Policy model. - * NOTE: in the near future, this will likely be removed and an API call to EPM will be used to retrieve - * the latest from the Endpoint package + * Return a new default `PolicyConfig`. */ -export const generatePolicy = (): PolicyConfig => { +export const factory = (): PolicyConfig => { return { windows: { events: { diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 403ca9832e191..7143f07d8c702 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -7,12 +7,15 @@ import { SearchResponse } from 'elasticsearch'; import { TypeOf } from '@kbn/config-schema'; import { alertingIndexGetQuerySchema } from './schema/alert_index'; +import { Datasource, NewDatasource } from '../../ingest_manager/common'; /** * A deep readonly type that will make all children of a given object readonly recursively */ export type Immutable = T extends undefined | null | boolean | string | number ? T + : unknown extends T + ? unknown : T extends Array ? ImmutableArray : T extends Map @@ -442,3 +445,125 @@ export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf< * Result of the validated query params when handling alert index requests. */ export type AlertingIndexGetQueryResult = TypeOf; + +/** + * Endpoint Policy configuration + */ +export interface PolicyConfig { + windows: { + events: { + dll_and_driver_load: boolean; + dns: boolean; + file: boolean; + network: boolean; + process: boolean; + registry: boolean; + security: boolean; + }; + malware: MalwareFields; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; + mac: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + malware: MalwareFields; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; + linux: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; +} + +/** + * Windows-specific policy configuration that is supported via the UI + */ +type WindowsPolicyConfig = Pick; + +/** + * Mac-specific policy configuration that is supported via the UI + */ +type MacPolicyConfig = Pick; + +/** + * Linux-specific policy configuration that is supported via the UI + */ +type LinuxPolicyConfig = Pick; + +/** + * The set of Policy configuration settings that are show/edited via the UI + */ +export interface UIPolicyConfig { + windows: WindowsPolicyConfig; + mac: MacPolicyConfig; + linux: LinuxPolicyConfig; +} + +interface PolicyConfigAdvancedOptions { + elasticsearch: { + indices: { + control: string; + event: string; + logging: string; + }; + kernel: { + connect: boolean; + process: boolean; + }; + }; +} + +/** Policy: Malware protection fields */ +export interface MalwareFields { + mode: ProtectionModes; +} + +/** Policy protection mode options */ +export enum ProtectionModes { + detect = 'detect', + prevent = 'prevent', + preventNotify = 'preventNotify', + off = 'off', +} + +/** + * Endpoint Policy data, which extends Ingest's `Datasource` type + */ +export type PolicyData = Datasource & NewPolicyData; + +/** + * New policy data. Used when updating the policy record via ingest APIs + */ +export type NewPolicyData = NewDatasource & { + inputs: [ + { + type: 'endpoint'; + enabled: boolean; + streams: []; + config: { + policy: { + value: PolicyConfig; + }; + }; + } + ]; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/index_pattern.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/index_pattern.ts new file mode 100644 index 0000000000000..0cae054432f96 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/index_pattern.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 { all } from 'deepmerge'; +import { Immutable } from '../../../../common/types'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +/** + * Model for the `IIndexPattern` interface exported by the `data` plugin. + */ +export function clone(value: IIndexPattern | Immutable): IIndexPattern { + return all([value]) as IIndexPattern; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts index bf96942e83a91..3e56b1ff14d65 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UIPolicyConfig } from '../types'; +import { UIPolicyConfig } from '../../../../common/types'; /** * A typed Object.entries() function where the keys and values are typed based on the given object diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts index 79e9de9c67352..feac8944f476b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts @@ -14,16 +14,17 @@ import { coreMock } from 'src/core/public/mocks'; import { DepsStartMock, depsStartMock } from '../../mocks'; import { createBrowserHistory } from 'history'; import { mockAlertResultList } from './mock_alert_result_list'; +import { Immutable } from '../../../../../common/types'; describe('alert details tests', () => { - let store: Store; + let store: Store, Immutable>; let coreStart: ReturnType; let depsStart: DepsStartMock; let history: History; /** * A function that waits until a selector returns true. */ - let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise; + let selectorIsTrue: (selector: (state: Immutable) => boolean) => Promise; beforeEach(() => { coreStart = coreMock.createStart(); depsStart = depsStartMock(); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts index b1cc2d46f614a..84281813312e0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts @@ -12,20 +12,20 @@ import { alertMiddlewareFactory } from './middleware'; import { AppAction } from '../action'; import { coreMock } from 'src/core/public/mocks'; import { DepsStartMock, depsStartMock } from '../../mocks'; -import { AlertResultList } from '../../../../../common/types'; +import { AlertResultList, Immutable } from '../../../../../common/types'; import { isOnAlertPage } from './selectors'; import { createBrowserHistory } from 'history'; import { mockAlertResultList } from './mock_alert_result_list'; describe('alert list tests', () => { - let store: Store; + let store: Store, Immutable>; let coreStart: ReturnType; let depsStart: DepsStartMock; let history: History; /** * A function that waits until a selector returns true. */ - let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise; + let selectorIsTrue: (selector: (state: Immutable) => boolean) => Promise; beforeEach(() => { coreStart = coreMock.createStart(); depsStart = depsStartMock(); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts index bb5893f14287b..4cc86e9c0449c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts @@ -15,9 +15,10 @@ import { DepsStartMock, depsStartMock } from '../../mocks'; import { createBrowserHistory } from 'history'; import { uiQueryParams } from './selectors'; import { urlFromQueryParams } from '../../view/alerts/url_from_query_params'; +import { Immutable } from '../../../../../common/types'; describe('alert list pagination', () => { - let store: Store; + let store: Store, Immutable>; let coreStart: ReturnType; let depsStart: DepsStartMock; let history: History; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts index 4430a4d39cf4a..52b91dcae7d70 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer } from 'redux'; -import { AlertListState } from '../../types'; +import { AlertListState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; +import { Immutable } from '../../../../../common/types'; -const initialState = (): AlertListState => { +const initialState = (): Immutable => { return { alerts: [], alertDetails: undefined, @@ -22,7 +22,7 @@ const initialState = (): AlertListState => { }; }; -export const alertListReducer: Reducer = ( +export const alertListReducer: ImmutableReducer = ( state = initialState(), action ) => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 5e9b08c09c2c7..cc362c3701956 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -19,23 +19,23 @@ const createStructuredSelector: CreateStructuredSelector = createStructuredSelec /** * Returns the Alert Data array from state */ -export const alertListData = (state: AlertListState) => state.alerts; +export const alertListData = (state: Immutable) => state.alerts; -export const selectedAlertDetailsData = (state: AlertListState) => state.alertDetails; +export const selectedAlertDetailsData = (state: Immutable) => state.alertDetails; /** * Returns the alert list pagination data from state */ export const alertListPagination = createStructuredSelector({ - pageIndex: (state: AlertListState) => state.pageIndex, - pageSize: (state: AlertListState) => state.pageSize, - total: (state: AlertListState) => state.total, + pageIndex: (state: Immutable) => state.pageIndex, + pageSize: (state: Immutable) => state.pageSize, + total: (state: Immutable) => state.total, }); /** * Returns a boolean based on whether or not the user is on the alerts page */ -export const isOnAlertPage = (state: AlertListState): boolean => { +export const isOnAlertPage = (state: Immutable): boolean => { return state.location ? state.location.pathname === '/alerts' : false; }; @@ -44,10 +44,10 @@ export const isOnAlertPage = (state: AlertListState): boolean => { * Used to calculate urls for links and such. */ export const uiQueryParams: ( - state: AlertListState + state: Immutable ) => Immutable = createSelector( - (state: AlertListState) => state.location, - (location: AlertListState['location']) => { + state => state.location, + (location: Immutable['location']) => { const data: AlertingIndexUIQueryParams = {}; if (location) { // Removes the `?` from the beginning of query string if it exists @@ -82,7 +82,7 @@ export const uiQueryParams: ( * Parses the ui query params and returns a object that represents the query used by the SearchBar component. * If the query url param is undefined, a default is returned. */ -export const searchBarQuery: (state: AlertListState) => Query = createSelector( +export const searchBarQuery: (state: Immutable) => Query = createSelector( uiQueryParams, ({ query }) => { if (query !== undefined) { @@ -97,21 +97,20 @@ export const searchBarQuery: (state: AlertListState) => Query = createSelector( * Parses the ui query params and returns a rison encoded string that represents the search bar's date range. * A default is provided if 'date_range' is not present in the url params. */ -export const encodedSearchBarDateRange: (state: AlertListState) => string = createSelector( - uiQueryParams, - ({ date_range: dateRange }) => { - if (dateRange === undefined) { - return encode({ from: 'now-24h', to: 'now' }); - } else { - return dateRange; - } +export const encodedSearchBarDateRange: ( + state: Immutable +) => string = createSelector(uiQueryParams, ({ date_range: dateRange }) => { + if (dateRange === undefined) { + return encode({ from: 'now-24h', to: 'now' }); + } else { + return dateRange; } -); +}); /** * Parses the ui query params and returns a object that represents the dateRange used by the SearchBar component. */ -export const searchBarDateRange: (state: AlertListState) => TimeRange = createSelector( +export const searchBarDateRange: (state: Immutable) => TimeRange = createSelector( encodedSearchBarDateRange, encodedDateRange => { return (decode(encodedDateRange) as unknown) as TimeRange; @@ -122,7 +121,7 @@ export const searchBarDateRange: (state: AlertListState) => TimeRange = createSe * Parses the ui query params and returns an array of filters used by the SearchBar component. * If the 'filters' param is not present, a default is returned. */ -export const searchBarFilters: (state: AlertListState) => Filter[] = createSelector( +export const searchBarFilters: (state: Immutable) => Filter[] = createSelector( uiQueryParams, ({ filters }) => { if (filters !== undefined) { @@ -136,13 +135,14 @@ export const searchBarFilters: (state: AlertListState) => Filter[] = createSelec /** * Returns the indexPatterns used by the SearchBar component */ -export const searchBarIndexPatterns = (state: AlertListState) => state.searchBar.patterns; +export const searchBarIndexPatterns = (state: Immutable) => + state.searchBar.patterns; /** * query params to use when requesting alert data. */ export const apiQueryParams: ( - state: AlertListState + state: Immutable ) => Immutable = createSelector( uiQueryParams, encodedSearchBarDateRange, @@ -161,7 +161,7 @@ export const apiQueryParams: ( * True if the user has selected an alert to see details about. * Populated via the browsers query params. */ -export const hasSelectedAlert: (state: AlertListState) => boolean = createSelector( +export const hasSelectedAlert: (state: Immutable) => boolean = createSelector( uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts index 4dafa68ddb647..21871ec8ca849 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -27,14 +27,8 @@ interface UserPaginatedHostList { payload: HostListPagination; } -// Why is FakeActionWithNoPayload here, see: https://github.com/elastic/endpoint-app-team/issues/273 -interface FakeActionWithNoPayload { - type: 'fakeActionWithNoPayLoad'; -} - export type HostAction = | ServerReturnedHostList | ServerReturnedHostDetails | ServerFailedToReturnHostDetails - | UserPaginatedHostList - | FakeActionWithNoPayload; + | UserPaginatedHostList; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index 8c8578426aa29..8f39baddda00e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { CoreStart, HttpSetup } from 'kibana/public'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; +import { applyMiddleware, createStore, Store } from 'redux'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; import { hostListReducer, hostMiddlewareFactory } from './index'; -import { HostResultList } from '../../../../../common/types'; +import { HostResultList, Immutable } from '../../../../../common/types'; import { HostListState } from '../../types'; import { AppAction } from '../action'; import { listData } from './selectors'; @@ -20,9 +20,10 @@ describe('host list middleware', () => { let fakeCoreStart: jest.Mocked; let depsStart: DepsStartMock; let fakeHttpServices: jest.Mocked; - let store: Store; - let getState: typeof store['getState']; - let dispatch: Dispatch; + type HostListStore = Store, Immutable>; + let store: HostListStore; + let getState: HostListStore['getState']; + let dispatch: HostListStore['dispatch']; let history: History; const getEndpointListApiResponse = (): HostResultList => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index ad6741dab7be7..298e819645dbe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer } from 'redux'; -import { HostListState } from '../../types'; +import { HostListState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; const initialState = (): HostListState => { @@ -21,7 +20,7 @@ const initialState = (): HostListState => { }; }; -export const hostListReducer: Reducer = ( +export const hostListReducer: ImmutableReducer = ( state = initialState(), action ) => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts index ebe310cb51190..35bf5d0616878 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -8,30 +8,30 @@ import { createSelector } from 'reselect'; import { Immutable } from '../../../../../common/types'; import { HostListState, HostIndexUIQueryParams } from '../../types'; -export const listData = (state: HostListState) => state.hosts; +export const listData = (state: Immutable) => state.hosts; -export const pageIndex = (state: HostListState) => state.pageIndex; +export const pageIndex = (state: Immutable) => state.pageIndex; -export const pageSize = (state: HostListState) => state.pageSize; +export const pageSize = (state: Immutable) => state.pageSize; -export const totalHits = (state: HostListState) => state.total; +export const totalHits = (state: Immutable) => state.total; -export const isLoading = (state: HostListState) => state.loading; +export const isLoading = (state: Immutable) => state.loading; -export const detailsError = (state: HostListState) => state.detailsError; +export const detailsError = (state: Immutable) => state.detailsError; -export const detailsData = (state: HostListState) => { +export const detailsData = (state: Immutable) => { return state.details; }; -export const isOnHostPage = (state: HostListState) => +export const isOnHostPage = (state: Immutable) => state.location ? state.location.pathname === '/hosts' : false; export const uiQueryParams: ( - state: HostListState + state: Immutable ) => Immutable = createSelector( - (state: HostListState) => state.location, - (location: HostListState['location']) => { + (state: Immutable) => state.location, + (location: Immutable['location']) => { const data: HostIndexUIQueryParams = {}; if (location) { // Removes the `?` from the beginning of query string if it exists @@ -52,7 +52,7 @@ export const uiQueryParams: ( } ); -export const hasSelectedHost: (state: HostListState) => boolean = createSelector( +export const hasSelectedHost: (state: Immutable) => boolean = createSelector( uiQueryParams, ({ selected_host: selectedHost }) => { return selectedHost !== undefined; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/immutable_combine_reducers.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/immutable_combine_reducers.ts new file mode 100644 index 0000000000000..6895f0106fb5d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/immutable_combine_reducers.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 { combineReducers } from 'redux'; +import { ImmutableCombineReducers } from '../types'; + +/** + * Works the same as `combineReducers` from `redux`, but uses the `ImmutableCombineReducers` type. + */ +export const immutableCombineReducers: ImmutableCombineReducers = combineReducers; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts index 9905145048a8a..4de3dac02a8ec 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyData, PolicyDetailsState, ServerApiError, UIPolicyConfig } from '../../types'; +import { PolicyDetailsState, ServerApiError } from '../../types'; import { GetAgentStatusResponse } from '../../../../../../ingest_manager/common/types/rest_spec'; +import { PolicyData, UIPolicyConfig } from '../../../../../common/types'; interface ServerReturnedPolicyDetailsData { type: 'serverReturnedPolicyDetailsData'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts index f81852d6a074a..a24687ebbcbbc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts @@ -9,7 +9,7 @@ import { createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction } from './index'; import { policyConfig } from './selectors'; import { clone } from '../../models/policy_details_config'; -import { generatePolicy } from '../../models/policy'; +import { factory as policyConfigFactory } from '../../../../../common/models/policy_config'; describe('policy details: ', () => { let store: Store; @@ -38,7 +38,7 @@ describe('policy details: ', () => { streams: [], config: { policy: { - value: generatePolicy(), + value: policyConfigFactory(), }, }, }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts index a00ce255cbac4..7a3fbe7f23b9b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts @@ -4,19 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - MiddlewareFactory, - PolicyData, - PolicyDetailsState, - UpdateDatasourceResponse, -} from '../../types'; +import { MiddlewareFactory, PolicyDetailsState, UpdatePolicyResponse } from '../../types'; import { policyIdFromParams, isOnPolicyDetailsPage, policyDetails } from './selectors'; -import { generatePolicy } from '../../models/policy'; import { sendGetDatasource, sendGetFleetAgentStatusForConfig, sendPutDatasource, } from '../policy_list/services/ingest'; +import { PolicyData } from '../../../../../common/types'; +import { factory as policyConfigFactory } from '../../../../../common/models/policy_config'; export const policyDetailsMiddlewareFactory: MiddlewareFactory = coreStart => { const http = coreStart.http; @@ -49,7 +45,7 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory { return { @@ -23,7 +23,7 @@ const initialPolicyDetailsState = (): PolicyDetailsState => { }; }; -export const policyDetailsReducer: Reducer = ( +export const policyDetailsReducer: ImmutableReducer = ( state = initialPolicyDetailsState(), action ) => { @@ -70,7 +70,7 @@ export const policyDetailsReducer: Reducer = ( } if (action.type === 'userChangedUrl') { - const newState = { + const newState: Immutable = { ...state, location: action.payload, }; @@ -79,8 +79,10 @@ export const policyDetailsReducer: Reducer = ( // Did user just enter the Detail page? if so, then set the loading indicator and return new state if (isCurrentlyOnDetailsPage && !wasPreviouslyOnDetailsPage) { - newState.isLoading = true; - return newState; + return { + ...newState, + isLoading: true, + }; } return { ...initialPolicyDetailsState(), @@ -93,10 +95,19 @@ export const policyDetailsReducer: Reducer = ( return state; } const newState = { ...state, policyItem: { ...state.policyItem } }; - const newPolicy: any = { ...fullPolicy(state) }; + const newPolicy: PolicyConfig = { ...fullPolicy(state) }; + + /** + * This is directly changing redux state because `policyItem.inputs` was copied over and not cloned. + */ + // @ts-ignore newState.policyItem.inputs[0].config.policy.value = newPolicy; Object.entries(action.payload.policyConfig).forEach(([section, newSettings]) => { + /** + * this is not safe because `action.payload.policyConfig` may have excess keys + */ + // @ts-ignore newPolicy[section as keyof UIPolicyConfig] = { ...newPolicy[section as keyof UIPolicyConfig], ...newSettings, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts index a37a06bafcf05..98e129132e1cb 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts @@ -5,14 +5,15 @@ */ import { createSelector } from 'reselect'; -import { PolicyConfig, PolicyDetailsState, UIPolicyConfig } from '../../types'; -import { generatePolicy } from '../../models/policy'; +import { PolicyDetailsState } from '../../types'; +import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../common/types'; +import { factory as policyConfigFactory } from '../../../../../common/models/policy_config'; /** Returns the policy details */ -export const policyDetails = (state: PolicyDetailsState) => state.policyItem; +export const policyDetails = (state: Immutable) => state.policyItem; /** Returns a boolean of whether the user is on the policy details page or not */ -export const isOnPolicyDetailsPage = (state: PolicyDetailsState) => { +export const isOnPolicyDetailsPage = (state: Immutable) => { if (state.location) { const pathnameParts = state.location.pathname.split('/'); return pathnameParts[1] === 'policy' && pathnameParts[2]; @@ -32,14 +33,16 @@ export const policyIdFromParams: (state: PolicyDetailsState) => string = createS } ); +const defaultFullPolicy: Immutable = policyConfigFactory(); + /** * Returns the full Endpoint Policy, which will include private settings not shown on the UI. * Note: this will return a default full policy if the `policyItem` is `undefined` */ -export const fullPolicy: (s: PolicyDetailsState) => PolicyConfig = createSelector( +export const fullPolicy: (s: Immutable) => PolicyConfig = createSelector( policyDetails, policyData => { - return policyData?.inputs[0]?.config?.policy?.value ?? generatePolicy(); + return policyData?.inputs[0]?.config?.policy?.value ?? defaultFullPolicy; } ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts index 3db224f049c05..4c379b7426461 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyData, ServerApiError } from '../../types'; +import { ServerApiError } from '../../types'; +import { PolicyData } from '../../../../../common/types'; interface ServerReturnedPolicyListData { type: 'serverReturnedPolicyListData'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts index 4d153b5e03cd2..97a2b65fb65f8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts @@ -5,7 +5,7 @@ */ import { EndpointAppLocation, PolicyListState } from '../../types'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; +import { applyMiddleware, createStore, Store } from 'redux'; import { AppAction } from '../action'; import { policyListReducer } from './reducer'; import { policyListMiddlewareFactory } from './middleware'; @@ -18,13 +18,15 @@ import { setPolicyListApiMockImplementation, } from './test_mock_utils'; import { INGEST_API_DATASOURCES } from './services/ingest'; +import { Immutable } from '../../../../../common/types'; describe('policy list store concerns', () => { let fakeCoreStart: ReturnType; let depsStart: DepsStartMock; - let store: Store; - let getState: typeof store['getState']; - let dispatch: Dispatch; + type PolicyListStore = Store, Immutable>; + let store: PolicyListStore; + let getState: PolicyListStore['getState']; + let dispatch: PolicyListStore['dispatch']; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; beforeEach(() => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts index c073d26a676f0..cc771a4619e78 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MiddlewareFactory, PolicyListState, GetDatasourcesResponse } from '../../types'; +import { GetPolicyListResponse, MiddlewareFactory, PolicyListState } from '../../types'; import { sendGetEndpointSpecificDatasources } from './services/ingest'; import { isOnPolicyListPage, urlSearchParams } from './selectors'; @@ -18,7 +18,7 @@ export const policyListMiddlewareFactory: MiddlewareFactory = c if (action.type === 'userChangedUrl' && isOnPolicyListPage(state)) { const { page_index: pageIndex, page_size: pageSize } = urlSearchParams(state); - let response: GetDatasourcesResponse; + let response: GetPolicyListResponse; try { response = await sendGetEndpointSpecificDatasources(http, { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts index 30c1deac7f5e1..ccd3f84dd060c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer } from 'redux'; -import { PolicyListState } from '../../types'; +import { PolicyListState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; import { isOnPolicyListPage } from './selectors'; +import { Immutable } from '../../../../../common/types'; const initialPolicyListState = (): PolicyListState => { return { @@ -21,7 +21,7 @@ const initialPolicyListState = (): PolicyListState => { }; }; -export const policyListReducer: Reducer = ( +export const policyListReducer: ImmutableReducer = ( state = initialPolicyListState(), action ) => { @@ -42,7 +42,7 @@ export const policyListReducer: Reducer = ( } if (action.type === 'userChangedUrl') { - const newState = { + const newState: Immutable = { ...state, location: action.payload, }; @@ -53,14 +53,15 @@ export const policyListReducer: Reducer = ( // Also adjust some state if user is just entering the policy list view if (isCurrentlyOnListPage) { if (!wasPreviouslyOnListPage) { - newState.apiError = undefined; - newState.isLoading = true; + return { + ...newState, + apiError: undefined, + isLoading: true, + }; } return newState; } - return { - ...initialPolicyListState(), - }; + return initialPolicyListState(); } return state; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts index ce13d89b2b8c2..6d2e952fa07bb 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts @@ -7,32 +7,33 @@ import { createSelector } from 'reselect'; import { parse } from 'query-string'; import { PolicyListState, PolicyListUrlSearchParams } from '../../types'; +import { Immutable } from '../../../../../common/types'; const PAGE_SIZES = Object.freeze([10, 20, 50]); -export const selectPolicyItems = (state: PolicyListState) => state.policyItems; +export const selectPolicyItems = (state: Immutable) => state.policyItems; -export const selectPageIndex = (state: PolicyListState) => state.pageIndex; +export const selectPageIndex = (state: Immutable) => state.pageIndex; -export const selectPageSize = (state: PolicyListState) => state.pageSize; +export const selectPageSize = (state: Immutable) => state.pageSize; -export const selectTotal = (state: PolicyListState) => state.total; +export const selectTotal = (state: Immutable) => state.total; -export const selectIsLoading = (state: PolicyListState) => state.isLoading; +export const selectIsLoading = (state: Immutable) => state.isLoading; -export const selectApiError = (state: PolicyListState) => state.apiError; +export const selectApiError = (state: Immutable) => state.apiError; -export const isOnPolicyListPage = (state: PolicyListState) => { +export const isOnPolicyListPage = (state: Immutable) => { return state.location?.pathname === '/policy'; }; -const routeLocation = (state: PolicyListState) => state.location; +const routeLocation = (state: Immutable) => state.location; /** * Returns the supported URL search params, populated with defaults if none where present in the URL */ export const urlSearchParams: ( - state: PolicyListState + state: Immutable ) => PolicyListUrlSearchParams = createSelector(routeLocation, location => { const searchParams = { page_index: 0, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts index bfbb5f94e8950..4356517e43c2c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts @@ -9,12 +9,8 @@ import { GetDatasourcesRequest, GetAgentStatusResponse, } from '../../../../../../../ingest_manager/common'; -import { - NewPolicyData, - GetDatasourcesResponse, - GetDatasourceResponse, - UpdateDatasourceResponse, -} from '../../../types'; +import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; +import { NewPolicyData } from '../../../../../../common/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; @@ -30,8 +26,8 @@ const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; export const sendGetEndpointSpecificDatasources = ( http: HttpStart, options: HttpFetchOptions & Partial = {} -): Promise => { - return http.get(INGEST_API_DATASOURCES, { +): Promise => { + return http.get(INGEST_API_DATASOURCES, { ...options, query: { ...options.query, @@ -53,7 +49,7 @@ export const sendGetDatasource = ( datasourceId: string, options?: HttpFetchOptions ) => { - return http.get(`${INGEST_API_DATASOURCES}/${datasourceId}`, options); + return http.get(`${INGEST_API_DATASOURCES}/${datasourceId}`, options); }; /** @@ -69,7 +65,7 @@ export const sendPutDatasource = ( datasourceId: string, datasource: NewPolicyData, options: Exclude = {} -): Promise => { +): Promise => { return http.put(`${INGEST_API_DATASOURCES}/${datasourceId}`, { ...options, body: JSON.stringify(datasource), diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts index 0d41ae0d76da4..20d5a637182d2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts @@ -8,7 +8,7 @@ import { HttpStart } from 'kibana/public'; import { Dispatch } from 'redux'; import { INGEST_API_DATASOURCES } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; -import { AppAction, GetDatasourcesResponse, GlobalState, MiddlewareFactory } from '../../types'; +import { AppAction, GetPolicyListResponse, GlobalState, MiddlewareFactory } from '../../types'; const generator = new EndpointDocGenerator('policy-list'); @@ -19,13 +19,13 @@ const generator = new EndpointDocGenerator('policy-list'); */ export const setPolicyListApiMockImplementation = ( mockedHttpService: jest.Mocked, - responseItems: GetDatasourcesResponse['items'] = [generator.generatePolicyDatasource()] + responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyDatasource()] ): void => { mockedHttpService.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { if (path === INGEST_API_DATASOURCES) { - return Promise.resolve({ + return Promise.resolve({ items: responseItems, total: 10, page: 1, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts index c8b2d08676724..2f77c380d9387 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -3,15 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { combineReducers, Reducer } from 'redux'; + import { hostListReducer } from './hosts'; import { AppAction } from './action'; import { alertListReducer } from './alerts'; -import { GlobalState } from '../types'; +import { GlobalState, ImmutableReducer } from '../types'; import { policyListReducer } from './policy_list'; import { policyDetailsReducer } from './policy_details'; +import { immutableCombineReducers } from './immutable_combine_reducers'; -export const appReducer: Reducer = combineReducers({ +export const appReducer: ImmutableReducer = immutableCombineReducers({ hostList: hostListReducer, alertList: alertListReducer, policyList: policyListReducer, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 54afbf220944e..7aca94d3e9c7c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch, MiddlewareAPI } from 'redux'; +import { Dispatch, MiddlewareAPI, Action as ReduxAction, AnyAction as ReduxAnyAction } from 'redux'; import { IIndexPattern } from 'src/plugins/data/public'; import { HostMetadata, @@ -13,15 +13,19 @@ import { Immutable, ImmutableArray, AlertDetails, + MalwareFields, + UIPolicyConfig, + PolicyData, } from '../../../common/types'; import { EndpointPluginStartDependencies } from '../../plugin'; import { AppAction } from './store/action'; import { CoreStart } from '../../../../../../src/core/public'; -import { Datasource, NewDatasource } from '../../../../ingest_manager/common/types/models'; import { GetAgentStatusResponse, - CreateDatasourceResponse, -} from '../../../../ingest_manager/common/types/rest_spec'; + GetDatasourcesResponse, + GetOneDatasourceResponse, + UpdateDatasourceResponse, +} from '../../../../ingest_manager/common'; export { AppAction }; export type MiddlewareFactory = ( @@ -56,29 +60,6 @@ export interface ServerApiError { message: string; } -/** - * New policy data. Used when updating the policy record via ingest APIs - */ -export type NewPolicyData = NewDatasource & { - inputs: [ - { - type: 'endpoint'; - enabled: boolean; - streams: []; - config: { - policy: { - value: PolicyConfig; - }; - }; - } - ]; -}; - -/** - * Endpoint Policy data, which extends Ingest's `Datasource` type - */ -export type PolicyData = Datasource & NewPolicyData; - /** * Policy list store state */ @@ -189,30 +170,6 @@ interface PolicyConfigAdvancedOptions { }; } -/** - * Windows-specific policy configuration that is supported via the UI - */ -type WindowsPolicyConfig = Pick; - -/** - * Mac-specific policy configuration that is supported via the UI - */ -type MacPolicyConfig = Pick; - -/** - * Linux-specific policy configuration that is supported via the UI - */ -type LinuxPolicyConfig = Pick; - -/** - * The set of Policy configuration settings that are show/edited via the UI - */ -export interface UIPolicyConfig { - windows: WindowsPolicyConfig; - mac: MacPolicyConfig; - linux: LinuxPolicyConfig; -} - /** OS used in Policy */ export enum OS { windows = 'windows', @@ -243,20 +200,7 @@ export type KeysByValueCriteria = { }[keyof O]; /** Returns an array of the policy OSes that have a malware protection field */ - export type MalwareProtectionOSes = KeysByValueCriteria; -/** Policy: Malware protection fields */ -export interface MalwareFields { - mode: ProtectionModes; -} - -/** Policy protection mode options */ -export enum ProtectionModes { - detect = 'detect', - prevent = 'prevent', - preventNotify = 'preventNotify', - off = 'off', -} export interface GlobalState { readonly hostList: HostListState; @@ -335,24 +279,39 @@ export interface AlertingIndexUIQueryParams { filters?: string; } -export interface GetDatasourcesResponse { +export interface GetPolicyListResponse extends GetDatasourcesResponse { items: PolicyData[]; - total: number; - page: number; - perPage: number; - success: boolean; } -export interface GetDatasourceResponse { +export interface GetPolicyResponse extends GetOneDatasourceResponse { item: PolicyData; - success: boolean; } -export type UpdateDatasourceResponse = CreateDatasourceResponse & { +export interface UpdatePolicyResponse extends UpdateDatasourceResponse { item: PolicyData; -}; +} + +/** + * Like `Reducer` from `redux` but it accepts immutable versions of `state` and `action`. + * Use this type for all Reducers in order to help enforce our pattern of immutable state. + */ +export type ImmutableReducer = ( + state: Immutable | undefined, + action: Immutable +) => State | Immutable; /** - * The PageId type is used for the payload when firing userNavigatedToPage actions + * A alternate interface for `redux`'s `combineReducers`. Will work with the same underlying implementation, + * but will enforce that `Immutable` versions of `state` and `action` are received. */ -export type PageId = 'alertsPage' | 'managementPage' | 'policyListPage'; +export type ImmutableCombineReducers = ( + reducers: ImmutableReducersMapObject +) => ImmutableReducer; + +/** + * Like `redux`'s `ReducersMapObject` (which is used by `combineReducers`) but enforces that + * the `state` and `action` received are `Immutable` versions. + */ +type ImmutableReducersMapObject = { + [K in keyof S]: ImmutableReducer; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx index 5b872962a5dc0..1ede06c086517 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { memo, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { encode, RisonValue } from 'rison-node'; @@ -14,11 +14,18 @@ import { urlFromQueryParams } from './url_from_query_params'; import { useAlertListSelector } from './hooks/use_alerts_selector'; import * as selectors from '../../store/alerts/selectors'; import { EndpointPluginServices } from '../../../../plugin'; +import { clone } from '../../models/index_pattern'; export const AlertIndexSearchBar = memo(() => { const history = useHistory(); const queryParams = useAlertListSelector(selectors.uiQueryParams); const searchBarIndexPatterns = useAlertListSelector(selectors.searchBarIndexPatterns); + + // Deeply clone the search bar index patterns as the receiving component may mutate them + const clonedSearchBarIndexPatterns = useMemo( + () => searchBarIndexPatterns.map(pattern => clone(pattern)), + [searchBarIndexPatterns] + ); const searchBarQuery = useAlertListSelector(selectors.searchBarQuery); const searchBarDateRange = useAlertListSelector(selectors.searchBarDateRange); const searchBarFilters = useAlertListSelector(selectors.searchBarFilters); @@ -68,7 +75,7 @@ export const AlertIndexSearchBar = memo(() => { dataTestSubj="alertsSearchBar" appName="endpoint" isLoading={false} - indexPatterns={searchBarIndexPatterns} + indexPatterns={clonedSearchBarIndexPatterns} query={searchBarQuery} dateRangeFrom={searchBarDateRange.from} dateRangeTo={searchBarDateRange.to} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx index 94625b8c66191..1d81d6e8a16db 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -23,12 +23,14 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; +import { EuiBasicTableColumn } from '@elastic/eui'; import { HostDetailsFlyout } from './details'; import * as selectors from '../../store/hosts/selectors'; import { HostAction } from '../../store/hosts/action'; import { useHostListSelector } from './hooks'; import { CreateStructuredSelector } from '../../types'; import { urlFromQueryParams } from './url_from_query_params'; +import { HostMetadata, Immutable } from '../../../../../common/types'; const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const HostList = () => { @@ -65,7 +67,7 @@ export const HostList = () => { [dispatch] ); - const columns = useMemo(() => { + const columns: Array>> = useMemo(() => { return [ { field: '', @@ -174,7 +176,7 @@ export const HostList = () => { [...listData], [listData])} columns={columns} loading={isLoading} pagination={paginationSetup} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx index 8b6c32c3277ed..341086c7cf75c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiCard, EuiFlexGroup, @@ -25,14 +25,27 @@ const PolicyDetailCard = styled.div` } `; export const ConfigForm: React.FC<{ + /** + * A subtitle for this component. + **/ type: string; - supportedOss: string[]; + /** + * Types of supported operating systems. + */ + supportedOss: React.ReactNode; children: React.ReactNode; - id: string; - /** Takes a react component to be put on the right corner of the card */ + /** + * A description for the component. + */ + description: string; + /** + * The `data-test-subj` attribute to append to a certain child element. + */ + dataTestSubj: string; + /** React Node to be put on the right corner of the card */ rightCorner: React.ReactNode; -}> = React.memo(({ type, supportedOss, children, id, rightCorner }) => { - const typeTitle = () => { +}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner, description }) => { + const typeTitle = useMemo(() => { return ( @@ -59,28 +72,25 @@ export const ConfigForm: React.FC<{ - {supportedOss.join(', ')} + {supportedOss} {rightCorner} ); - }; + }, [rightCorner, supportedOss, type]); return ( - - {children} - - } - /> + title={typeTitle} + > + + {children} + ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx index bec6b33b85c7f..74322ac8b993b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx @@ -11,7 +11,7 @@ import { htmlIdGenerator } from '@elastic/eui'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { policyConfig } from '../../../../store/policy_details/selectors'; import { PolicyDetailsAction } from '../../../../store/policy_details'; -import { UIPolicyConfig } from '../../../../types'; +import { UIPolicyConfig } from '../../../../../../../common/types'; export const EventsCheckbox = React.memo(function({ name, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/linux.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/linux.tsx index 9d2ce03c20462..c3d6bdba7c852 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/linux.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/linux.tsx @@ -9,22 +9,23 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; -import { OS, UIPolicyConfig } from '../../../../types'; +import { OS } from '../../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../../models/policy_details_config'; +import { UIPolicyConfig } from '../../../../../../../common/types'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); const total = usePolicyDetailsSelector(totalLinuxEvents); - const checkboxes: Array<{ - name: string; - os: 'linux'; - protectionField: keyof UIPolicyConfig['linux']['events']; - }> = useMemo( - () => [ + const checkboxes = useMemo(() => { + const items: Array<{ + name: string; + os: 'linux'; + protectionField: keyof UIPolicyConfig['linux']['events']; + }> = [ { name: i18n.translate('xpack.endpoint.policyDetailsConfig.linux.events.file', { defaultMessage: 'File', @@ -46,11 +47,7 @@ export const LinuxEvents = React.memo(() => { os: OS.linux, protectionField: 'network', }, - ], - [] - ); - - const renderCheckboxes = () => { + ]; return ( <> @@ -62,7 +59,7 @@ export const LinuxEvents = React.memo(() => { - {checkboxes.map((item, index) => { + {items.map((item, index) => { return ( { })} ); - }; + }, []); - const collectionsEnabled = () => { + const collectionsEnabled = useMemo(() => { return ( { /> ); - }; + }, [selected, total]); return ( + description={i18n.translate('xpack.endpoint.policy.details.eventCollectionLabel', { + defaultMessage: 'Event Collection', + })} + supportedOss={i18n.translate('xpack.endpoint.policy.details.linux', { + defaultMessage: 'Linux', + })} + dataTestSubj="linuxEventingForm" + rightCorner={collectionsEnabled} + > + {checkboxes} + ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx index 3b69c21d2b150..40b80b9af0f65 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx @@ -9,22 +9,23 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; -import { OS, UIPolicyConfig } from '../../../../types'; +import { OS } from '../../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../../models/policy_details_config'; +import { UIPolicyConfig } from '../../../../../../../common/types'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); const total = usePolicyDetailsSelector(totalMacEvents); - const checkboxes: Array<{ - name: string; - os: 'mac'; - protectionField: keyof UIPolicyConfig['mac']['events']; - }> = useMemo( - () => [ + const checkboxes = useMemo(() => { + const items: Array<{ + name: string; + os: 'mac'; + protectionField: keyof UIPolicyConfig['mac']['events']; + }> = [ { name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.file', { defaultMessage: 'File', @@ -46,11 +47,7 @@ export const MacEvents = React.memo(() => { os: OS.mac, protectionField: 'network', }, - ], - [] - ); - - const renderCheckboxes = () => { + ]; return ( <> @@ -62,7 +59,7 @@ export const MacEvents = React.memo(() => { - {checkboxes.map((item, index) => { + {items.map((item, index) => { return ( { })} ); - }; + }, []); - const collectionsEnabled = () => { + const collectionsEnabled = useMemo(() => { return ( { /> ); - }; + }, [selected, total]); return ( + description={i18n.translate('xpack.endpoint.policy.details.eventCollectionLabel', { + defaultMessage: 'Event Collection', + })} + supportedOss={i18n.translate('xpack.endpoint.policy.details.mac', { defaultMessage: 'Mac' })} + dataTestSubj="macEventingForm" + rightCorner={collectionsEnabled} + > + {checkboxes} + ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx index da675dc1e2393..7f946de9614ca 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; -import { OS, UIPolicyConfig } from '../../../../types'; +import { OS } from '../../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, @@ -17,17 +17,18 @@ import { } from '../../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { setIn, getIn } from '../../../../models/policy_details_config'; +import { UIPolicyConfig, ImmutableArray } from '../../../../../../../common/types'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); const total = usePolicyDetailsSelector(totalWindowsEvents); - const checkboxes: Array<{ - name: string; - os: 'windows'; - protectionField: keyof UIPolicyConfig['windows']['events']; - }> = useMemo( - () => [ + const checkboxes = useMemo(() => { + const items: ImmutableArray<{ + name: string; + os: 'windows'; + protectionField: keyof UIPolicyConfig['windows']['events']; + }> = [ { name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', { defaultMessage: 'DLL and Driver Load', @@ -77,11 +78,7 @@ export const WindowsEvents = React.memo(() => { os: OS.windows, protectionField: 'security', }, - ], - [] - ); - - const renderCheckboxes = useMemo(() => { + ]; return ( <> @@ -93,7 +90,7 @@ export const WindowsEvents = React.memo(() => { - {checkboxes.map((item, index) => { + {items.map((item, index) => { return ( { })} ); - }, [checkboxes]); + }, []); const collectionsEnabled = useMemo(() => { return ( @@ -126,15 +123,16 @@ export const WindowsEvents = React.memo(() => { type={i18n.translate('xpack.endpoint.policy.details.eventCollection', { defaultMessage: 'Event Collection', })} - supportedOss={useMemo( - () => [ - i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }), - ], - [] - )} - id="windowsEventingForm" + description={i18n.translate('xpack.endpoint.policy.details.windowsLabel', { + defaultMessage: 'Windows', + })} + supportedOss={i18n.translate('xpack.endpoint.policy.details.windows', { + defaultMessage: 'Windows', + })} + dataTestSubj="windowsEventingForm" rightCorner={collectionsEnabled} - children={renderCheckboxes} - /> + > + {checkboxes} + ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx index 66b22178607b9..14871c71ec038 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx @@ -11,8 +11,8 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { htmlIdGenerator } from '@elastic/eui'; -import { Immutable } from '../../../../../../../common/types'; -import { OS, ProtectionModes, MalwareProtectionOSes } from '../../../../types'; +import { Immutable, ProtectionModes, ImmutableArray } from '../../../../../../../common/types'; +import { OS, MalwareProtectionOSes } from '../../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; @@ -73,7 +73,7 @@ export const MalwareProtections = React.memo(() => { // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; - const radios: Array<{ + const radios: ImmutableArray<{ id: ProtectionModes; label: string; protection: 'malware'; @@ -123,7 +123,7 @@ export const MalwareProtections = React.memo(() => { [dispatch, policyDetailsConfig] ); - const RadioButtons = () => { + const radioButtons = useMemo(() => { return ( <> @@ -148,9 +148,9 @@ export const MalwareProtections = React.memo(() => { ); - }; + }, [radios]); - const ProtectionSwitch = () => { + const protectionSwitch = useMemo(() => { return ( { onChange={handleSwitchChange} /> ); - }; + }, [handleSwitchChange, selected]); return ( + supportedOss={i18n.translate('xpack.endpoint.policy.details.windowsAndMac', { + defaultMessage: 'Windows, Mac', + })} + dataTestSubj="malwareProtectionsForm" + description={i18n.translate('xpack.endpoint.policy.details.malwareLabel', { + defaultMessage: 'Malware', + })} + rightCorner={protectionSwitch} + > + {radioButtons} + ); }); 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 295312fff01dd..062c7afb6706d 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 @@ -20,10 +20,10 @@ import { } from '../../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../../store/policy_list'; -import { PolicyData } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { PageView } from '../components/page_view'; import { LinkToApp } from '../components/link_to_app'; +import { Immutable, PolicyData } from '../../../../../common/types'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -44,8 +44,8 @@ const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) ); }; -const renderPolicyNameLink = (value: string, _item: PolicyData) => { - return ; +const renderPolicyNameLink = (value: string, item: Immutable) => { + return ; }; export const PolicyList = React.memo(() => { @@ -88,7 +88,7 @@ export const PolicyList = React.memo(() => { [history, location.pathname] ); - const columns: Array> = useMemo( + const columns: Array>> = useMemo( () => [ { field: 'name', @@ -160,7 +160,7 @@ export const PolicyList = React.memo(() => { } > [...policyItems], [policyItems])} columns={columns} loading={loading} pagination={paginationSetup} diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index ae26d7a7ece07..986486902c3fa 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -21,7 +21,7 @@ beforeEach(() => { clusterClient = elasticsearchServiceMock.createClusterClient(); clusterClientAdapter = new ClusterClientAdapter({ logger, - clusterClient, + clusterClientPromise: Promise.resolve(clusterClient), }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 36bc94edfca4e..409bb2d00e161 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -14,7 +14,7 @@ export type IClusterClientAdapter = PublicMethodsOf; export interface ConstructorOpts { logger: Logger; - clusterClient: EsClusterClient; + clusterClientPromise: Promise; } export interface QueryEventsBySavedObjectResult { @@ -26,11 +26,11 @@ export interface QueryEventsBySavedObjectResult { export class ClusterClientAdapter { private readonly logger: Logger; - private readonly clusterClient: EsClusterClient; + private readonly clusterClientPromise: Promise; constructor(opts: ConstructorOpts) { this.logger = opts.logger; - this.clusterClient = opts.clusterClient; + this.clusterClientPromise = opts.clusterClientPromise; } public async indexDocument(doc: any): Promise { @@ -201,7 +201,8 @@ export class ClusterClientAdapter { private async callEs(operation: string, body?: any): Promise { try { this.debug(`callEs(${operation}) calls:`, body); - const result = await this.clusterClient.callAsInternalUser(operation, body); + const clusterClient = await this.clusterClientPromise; + const result = await clusterClient.callAsInternalUser(operation, body); this.debug(`callEs(${operation}) result:`, result); return result; } catch (err) { diff --git a/x-pack/plugins/event_log/server/es/context.ts b/x-pack/plugins/event_log/server/es/context.ts index 144f44ac8e5ea..b8e351367b695 100644 --- a/x-pack/plugins/event_log/server/es/context.ts +++ b/x-pack/plugins/event_log/server/es/context.ts @@ -32,7 +32,7 @@ export function createEsContext(params: EsContextCtorParams): EsContext { export interface EsContextCtorParams { logger: Logger; - clusterClient: EsClusterClient; + clusterClientPromise: Promise; indexNameRoot: string; } @@ -50,7 +50,7 @@ class EsContextImpl implements EsContext { this.initialized = false; this.esAdapter = new ClusterClientAdapter({ logger: params.logger, - clusterClient: params.clusterClient, + clusterClientPromise: params.clusterClientPromise, }); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 2cc41354b4fbc..e5034f599f118 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -66,7 +66,9 @@ export class Plugin implements CorePlugin elasticsearch.legacy.client), }); this.eventLogService = new EventLogService({ diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 2e43ede2480ce..7de52840777f9 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -17,6 +17,9 @@ import { import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiExpression } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; import { MetricExpressionParams, Comparator, @@ -208,6 +211,37 @@ export const Expressions: React.FC = props => { } }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + // INFO: If there is metadata, you're in the metrics explorer context + const canAddConditions = !!alertsContext.metadata; + + if (!canAddConditions && !alertParams.criteria) { + return ( + <> + + + {' '} + + + + . + + } + color="warning" + iconType="help" + /> + + + ); + } + return ( <> @@ -224,6 +258,7 @@ export const Expressions: React.FC = props => { alertParams.criteria.map((e, idx) => { return ( 1} fields={derivedIndexPattern.fields} remove={removeExpression} @@ -246,61 +281,65 @@ export const Expressions: React.FC = props => { />
- - - + {canAddConditions && ( + + + + )}
- - - - - - {alertsContext.metadata && ( - - - + <> + + + + + + + + + )} ); @@ -308,6 +347,7 @@ export const Expressions: React.FC = props => { interface ExpressionRowProps { fields: IFieldType[]; + canEditAggField: boolean; expressionId: number; expression: MetricExpression; errors: IErrorObject; @@ -359,7 +399,9 @@ export const ExpressionRow: React.FC = props => { const updateThreshold = useCallback( t => { - setAlertParams(expressionId, { ...expression, threshold: t }); + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } }, [expressionId, expression, setAlertParams] ); @@ -378,17 +420,20 @@ export const ExpressionRow: React.FC = props => { {aggType !== 'count' && ( - ({ - normalizedType: f.type, - name: f.name, - }))} - aggType={aggType} - errors={errors} - onChangeSelectedAggField={updateMetric} - /> + {!props.canEditAggField && } + {props.canEditAggField && ( + ({ + normalizedType: f.type, + name: f.name, + }))} + aggType={aggType} + errors={errors} + onChangeSelectedAggField={updateMetric} + /> + )} )} @@ -420,6 +465,19 @@ export const ExpressionRow: React.FC = props => { ); }; +export const DisabledAggField = ({ text }: { text: string }) => { + return ( + + ); +}; + export const aggregationType: { [key: string]: any } = { avg: { text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index b897c03e89f82..3496ea782ee99 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -38,6 +38,10 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { fooVar: { value: 'foo-value' }, fooVar2: { value: [1, 2] }, }, + pkg_stream: { + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, }, { id: 'test-logs-bar', @@ -95,7 +99,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); - it('returns agent datasource config with flattened input and stream configs', () => { + it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ id: 'mock-datasource', namespace: 'default', @@ -105,34 +109,18 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { { type: 'test-logs', enabled: true, - inputVar: 'input-value', - inputVar3: { - testField: 'test', - }, streams: [ { id: 'test-logs-foo', enabled: true, dataset: 'foo', - fooVar: 'foo-value', - fooVar2: [1, 2], + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], }, { id: 'test-logs-bar', enabled: true, dataset: 'bar', - barVar: 'bar-value', - barVar2: [1, 2], - barVar3: [ - { - namespace: 'mockNamespace', - anotherProp: 'test', - }, - { - namespace: 'mockNamespace2', - anotherProp: 'test2', - }, - ], }, ], }, @@ -160,17 +148,13 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { { type: 'test-logs', enabled: true, - inputVar: 'input-value', - inputVar3: { - testField: 'test', - }, streams: [ { id: 'test-logs-foo', enabled: true, dataset: 'foo', - fooVar: 'foo-value', - fooVar2: [1, 2], + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], }, ], }, diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index 20bbbec8919d6..b509878b7f945 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -3,38 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { safeLoad } from 'js-yaml'; -import { - Datasource, - NewDatasource, - DatasourceConfigRecord, - DatasourceConfigRecordEntry, - FullAgentConfigDatasource, -} from '../types'; +import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; -const configReducer = ( - configResult: DatasourceConfigRecord, - configEntry: [string, DatasourceConfigRecordEntry] -): DatasourceConfigRecord => { - const [configName, { type: configType, value: configValue }] = configEntry; - if (configValue !== undefined && configValue !== '') { - if (configType === 'yaml') { - try { - const yamlValue = safeLoad(configValue); - if (yamlValue) { - configResult[configName] = yamlValue; - } - } catch (e) { - // Silently swallow parsing error - } - } else { - configResult[configName] = configValue; - } - } - return configResult; -}; - export const storedDatasourceToAgentDatasource = ( datasource: Datasource | NewDatasource ): FullAgentConfigDatasource => { @@ -50,14 +21,14 @@ export const storedDatasourceToAgentDatasource = ( .map(input => { const fullInput = { ...input, - ...Object.entries(input.config || {}).reduce(configReducer, {}), streams: input.streams .filter(stream => stream.enabled) .map(stream => { const fullStream = { ...stream, - ...Object.entries(stream.config || {}).reduce(configReducer, {}), + ...stream.pkg_stream, }; + delete fullStream.pkg_stream; delete fullStream.config; return fullStream; }), diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index ee4d24ab11777..48243a12120f9 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -23,6 +23,7 @@ export interface DatasourceInputStream { dataset: string; processors?: string[]; config?: DatasourceConfigRecord; + pkg_stream?: any; } export interface DatasourceInput { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts index 66f734b904cfc..61f1f15d49259 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts @@ -13,17 +13,22 @@ export interface GetDatasourcesRequest { }; } +export interface GetDatasourcesResponse { + items: Datasource[]; + total: number; + page: number; + perPage: number; + success: boolean; +} + export interface GetOneDatasourceRequest { params: { datasourceId: string; }; } -export interface GetDatasourcesResponse { - items: Datasource[]; - total: number; - page: number; - perPage: number; +export interface GetOneDatasourceResponse { + item: Datasource; success: boolean; } @@ -40,6 +45,8 @@ export type UpdateDatasourceRequest = GetOneDatasourceRequest & { body: NewDatasource; }; +export type UpdateDatasourceResponse = CreateDatasourceResponse; + export interface DeleteDatasourcesRequest { body: { datasourceIds: string[]; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index e4f7202aeee10..7520f88215efe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -72,7 +72,7 @@ const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId } {key} ) : ( - •••••••••••••••••••••••••• + ••••••••••••••••••••• )} @@ -151,11 +151,10 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Name', }), truncateText: true, - width: '300px', textOnly: true, render: (name: string) => { return ( - + {name} ); @@ -166,7 +165,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.secretTitle', { defaultMessage: 'Secret', }), - width: '245px', + width: '215px', render: (apiKeyId: string) => { return ; }, @@ -186,7 +185,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.createdAtTitle', { defaultMessage: 'Created on', }), - width: '200px', + width: '150px', render: (createdAt: string) => { return createdAt ? ( @@ -198,7 +197,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.activeTitle', { defaultMessage: 'Active', }), - width: '80px', + width: '70px', render: (active: boolean) => { return ( @@ -212,7 +211,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.actionsTitle', { defaultMessage: 'Actions', }), - width: '100px', + width: '70px', render: (_: any, apiKey: EnrollmentAPIKey) => { return ( apiKey.active && ( diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 7ae562cf130ab..56d6053a1451b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; +import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; import { appContextService, datasourceService } from '../../services'; import { ensureInstalledPackage } from '../../services/epm/packages'; @@ -75,6 +76,7 @@ export const createDatasourceHandler: RequestHandler< const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.adminClient.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; + const newData = { ...request.body }; try { // Make sure the datasource package is installed if (request.body.package?.name) { @@ -83,10 +85,18 @@ export const createDatasourceHandler: RequestHandler< pkgName: request.body.package.name, callCluster, }); + + newData.inputs = (await datasourceService.assignPackageStream( + { + pkgName: request.body.package.name, + pkgVersion: request.body.package.version, + }, + request.body.inputs + )) as TypeOf['inputs']; } // Create datasource - const datasource = await datasourceService.create(soClient, request.body, { user }); + const datasource = await datasourceService.create(soClient, newData, { user }); const body: CreateDatasourceResponse = { item: datasource, success: true }; return response.ok({ body, @@ -107,14 +117,33 @@ export const updateDatasourceHandler: RequestHandler< const soClient = context.core.savedObjects.client; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; try { - const datasource = await datasourceService.update( + const datasource = await datasourceService.get(soClient, request.params.datasourceId); + + if (!datasource) { + throw Boom.notFound('Datasource not found'); + } + + const newData = { ...request.body }; + const pkg = newData.package || datasource.package; + const inputs = newData.inputs || datasource.inputs; + if (pkg && (newData.inputs || newData.package)) { + newData.inputs = (await datasourceService.assignPackageStream( + { + pkgName: pkg.name, + pkgVersion: pkg.version, + }, + inputs + )) as TypeOf['inputs']; + } + + const updatedDatasource = await datasourceService.update( soClient, request.params.datasourceId, - request.body, + newData, { user } ); return response.ok({ - body: { item: datasource, success: true }, + body: { item: updatedDatasource, success: true }, }); } catch (e) { return response.customError({ diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 6800cb4056700..1c36fda36847d 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -137,6 +137,7 @@ export const savedObjectMappings = { dataset: { type: 'keyword' }, processors: { type: 'keyword' }, config: { type: 'flattened' }, + pkg_stream: { type: 'flattened' }, }, }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts new file mode 100644 index 0000000000000..09c59998388d1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { datasourceService } from './datasource'; + +async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { + if (dataset === 'dataset1') { + return [ + { + buffer: Buffer.from(` +type: log +metricset: ["dataset1"] +paths: +{{#each paths}} +- {{this}} +{{/each}} +`), + }, + ]; + } + return []; +} + +jest.mock('./epm/packages/assets', () => { + return { + getAssetsDataForPackageKey: mockedGetAssetsData, + }; +}); + +describe('Datasource service', () => { + describe('assignPackageStream', () => { + it('should work with cofig variables from the stream', async () => { + const inputs = await datasourceService.assignPackageStream( + { + pkgName: 'package', + pkgVersion: '1.0.0', + }, + [ + { + type: 'log', + enabled: true, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + }, + ], + }, + ] + ); + + expect(inputs).toEqual([ + { + type: 'log', + enabled: true, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + pkg_stream: { + metricset: ['dataset1'], + paths: ['/var/log/set.log'], + type: 'log', + }, + }, + ], + }, + ]); + }); + + it('should work with config variables at the input level', async () => { + const inputs = await datasourceService.assignPackageStream( + { + pkgName: 'package', + pkgVersion: '1.0.0', + }, + [ + { + type: 'log', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + }, + ], + }, + ] + ); + + expect(inputs).toEqual([ + { + type: 'log', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + pkg_stream: { + metricset: ['dataset1'], + paths: ['/var/log/set.log'], + type: 'log', + }, + }, + ], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 1b8f2a690b94d..f27252aaa9a84 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -4,16 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract } from 'src/core/server'; +import { safeLoad } from 'js-yaml'; import { AuthenticatedUser } from '../../../security/server'; -import { DeleteDatasourcesResponse, packageToConfigDatasource } from '../../common'; +import { + DeleteDatasourcesResponse, + packageToConfigDatasource, + DatasourceInput, + DatasourceInputStream, +} from '../../common'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; import { NewDatasource, Datasource, ListWithKuery } from '../types'; import { agentConfigService } from './agent_config'; import { getPackageInfo, getInstallation } from './epm/packages'; import { outputService } from './output'; +import { getAssetsDataForPackageKey } from './epm/packages/assets'; +import { createStream } from './epm/agent/agent'; const SAVED_OBJECT_TYPE = DATASOURCE_SAVED_OBJECT_TYPE; +function getDataset(st: string) { + return st.split('.')[1]; +} + class DatasourceService { public async create( soClient: SavedObjectsClientContract, @@ -187,6 +199,61 @@ class DatasourceService { } } } + + public async assignPackageStream( + pkgInfo: { pkgName: string; pkgVersion: string }, + inputs: DatasourceInput[] + ): Promise { + const inputsPromises = inputs.map(input => _assignPackageStreamToInput(pkgInfo, input)); + return Promise.all(inputsPromises); + } +} + +const _isAgentStream = (p: string) => !!p.match(/agent\/stream\/stream\.yml/); + +async function _assignPackageStreamToInput( + pkgInfo: { pkgName: string; pkgVersion: string }, + input: DatasourceInput +) { + const streamsPromises = input.streams.map(stream => + _assignPackageStreamToStream(pkgInfo, input, stream) + ); + + const streams = await Promise.all(streamsPromises); + return { ...input, streams }; +} + +async function _assignPackageStreamToStream( + pkgInfo: { pkgName: string; pkgVersion: string }, + input: DatasourceInput, + stream: DatasourceInputStream +) { + if (!stream.enabled) { + return { ...stream, pkg_stream: undefined }; + } + const dataset = getDataset(stream.dataset); + const assetsData = await getAssetsDataForPackageKey(pkgInfo, _isAgentStream, dataset); + + const [pkgStream] = assetsData; + if (!pkgStream || !pkgStream.buffer) { + throw new Error(`Stream template not found for dataset ${dataset}`); + } + + // Populate template variables from input config and stream config + const data: { [k: string]: string | string[] } = {}; + if (input.config) { + for (const key of Object.keys(input.config)) { + data[key] = input.config[key].value; + } + } + if (stream.config) { + for (const key of Object.keys(stream.config)) { + data[key] = stream.config[key].value; + } + } + const yaml = safeLoad(createStream(data, pkgStream.buffer.toString())); + stream.pkg_stream = yaml; + return { ...stream }; } export const datasourceService = new DatasourceService(); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts index 4f75ba0332418..21de625532f03 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -4,29 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import fs from 'fs'; -import * as yaml from 'js-yaml'; -import path from 'path'; -import { createInput } from './agent'; +import { createStream } from './agent'; -test('test converting input and manifest into template', () => { - const manifest = yaml.safeLoad( - fs.readFileSync(path.join(__dirname, 'tests/manifest.yml'), 'utf8') - ); +test('Test creating a stream from template', () => { + const streamTemplate = ` +input: log +paths: +{{#each paths}} + - {{this}} +{{/each}} +exclude_files: [".gz$"] +processors: + - add_locale: ~ + `; + const vars = { + paths: ['/usr/local/var/log/nginx/access.log'], + }; - const inputTemplate = fs.readFileSync(path.join(__dirname, 'tests/input.yml'), 'utf8'); - const output = createInput(manifest.vars, inputTemplate); + const output = createStream(vars, streamTemplate); - // Golden file path - const generatedFile = path.join(__dirname, './tests/input.generated.yaml'); - - // Regenerate the file if `-generate` flag is used - if (process.argv.includes('-generate')) { - fs.writeFileSync(generatedFile, output); - } - - const outputData = fs.readFileSync(generatedFile, 'utf-8'); - - // Check that content file and generated file are equal - expect(outputData).toBe(output); + expect(output).toBe(` +input: log +paths: + - /usr/local/var/log/nginx/access.log +exclude_files: [".gz$"] +processors: + - add_locale: ~ + `); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index c7dd3dab38bc1..5d9a6d409aa1a 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -5,19 +5,12 @@ */ import Handlebars from 'handlebars'; -import { RegistryVarsEntry } from '../../../types'; -/** - * This takes a dataset object as input and merges it with the input template. - * It returns the resolved template as a string. - */ -export function createInput(vars: RegistryVarsEntry[], inputTemplate: string): string { - const view: Record = {}; - - for (const v of vars) { - view[v.name] = v.default; - } +interface StreamVars { + [k: string]: string | string[]; +} - const template = Handlebars.compile(inputTemplate); - return template(view); +export function createStream(vars: StreamVars, streamTemplate: string) { + const template = Handlebars.compile(streamTemplate); + return template(vars); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.generated.yaml b/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.generated.yaml deleted file mode 100644 index 451ed554ce259..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.generated.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: log -paths: - - "/var/log/nginx/access.log*" - -tags: nginx diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.yml b/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.yml deleted file mode 100644 index 65a23fc2fa9ad..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.yml +++ /dev/null @@ -1,7 +0,0 @@ -type: log -paths: -{{#each paths}} - - "{{this}}" -{{/each}} - -tags: {{tags}} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/manifest.yml b/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/manifest.yml deleted file mode 100644 index 46a38179fe132..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/manifest.yml +++ /dev/null @@ -1,20 +0,0 @@ -title: Nginx Acess Logs -release: beta -type: logs -ingest_pipeline: default - -vars: - - name: paths - # Should we define this as array? How will the UI best make sense of it? - type: textarea - default: - - /var/log/nginx/access.log* - # I suggest to use ECS fields for this config options here: https://github.com/elastic/ecs/blob/master/schemas/os.yml - # This would need to be based on a predefined definition on what can be filtered on - os.darwin: - - /usr/local/var/log/nginx/access.log* - os.windows: - - c:/programdata/nginx/logs/*access.log* - - name: tags - default: [nginx] - type: text diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index 7026d9eae24c3..50d347c69cc24 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -71,3 +71,13 @@ export async function getAssetsData( return entries; } + +export async function getAssetsDataForPackageKey( + { pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }, + filter = (path: string): boolean => true, + datasetName?: string +): Promise { + const registryPkgInfo = await Registry.fetchInfo(pkgName, pkgVersion); + + return getAssetsData(registryPkgInfo, filter, datasetName); +} diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index bbaf083fb8396..167a24481aba5 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -123,8 +123,18 @@ async function addPackageToConfig( pkgName: packageToInstall.name, pkgVersion: packageToInstall.version, }); - await datasourceService.create( - soClient, - packageToConfigDatasource(packageInfo, config.id, defaultOutput.id, undefined, config.namespace) + + const newDatasource = packageToConfigDatasource( + packageInfo, + config.id, + defaultOutput.id, + undefined, + config.namespace + ); + newDatasource.inputs = await datasourceService.assignPackageStream( + { pkgName: packageToInstall.name, pkgVersion: packageToInstall.version }, + newDatasource.inputs ); + + await datasourceService.create(soClient, newDatasource); } diff --git a/x-pack/legacy/plugins/reporting/server/config/config.js b/x-pack/plugins/lens/config.ts similarity index 53% rename from x-pack/legacy/plugins/reporting/server/config/config.js rename to x-pack/plugins/lens/config.ts index 08e4db464b003..84cf02a7ea541 100644 --- a/x-pack/legacy/plugins/reporting/server/config/config.js +++ b/x-pack/plugins/lens/config.ts @@ -4,18 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cpus } from 'os'; +import { schema, TypeOf } from '@kbn/config-schema'; -const defaultCPUCount = 2; +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); -function cpuCount() { - try { - return cpus().length; - } catch (e) { - return defaultCPUCount; - } -} - -export const config = { - concurrency: cpuCount(), -}; +export type ConfigSchema = TypeOf; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 6abdaad7903be..ce544b31b88ef 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -3,7 +3,15 @@ "version": "8.0.0", "kibanaVersion": "kibana", "server": true, - "ui": false, - "optionalPlugins": ["usageCollection", "taskManager"], + "ui": true, + "requiredPlugins": [ + "data", + "expressions", + "navigation", + "kibanaLegacy", + "uiActions", + "visualizations" + ], + "optionalPlugins": ["embeddable", "usageCollection", "taskManager"], "configPath": ["xpack", "lens"] } diff --git a/x-pack/legacy/plugins/lens/public/_mixins.scss b/x-pack/plugins/lens/public/_mixins.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/_mixins.scss rename to x-pack/plugins/lens/public/_mixins.scss diff --git a/x-pack/legacy/plugins/lens/public/_variables.scss b/x-pack/plugins/lens/public/_variables.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/_variables.scss rename to x-pack/plugins/lens/public/_variables.scss diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss b/x-pack/plugins/lens/public/app_plugin/_app.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/app_plugin/_app.scss rename to x-pack/plugins/lens/public/app_plugin/_app.scss diff --git a/x-pack/plugins/lens/public/app_plugin/_index.scss b/x-pack/plugins/lens/public/app_plugin/_index.scss new file mode 100644 index 0000000000000..e72e824224956 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/_index.scss @@ -0,0 +1 @@ +@import 'app'; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx rename to x-pack/plugins/lens/public/app_plugin/app.test.tsx index be72dd4b4edef..41d0e3a7aa9a0 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -9,33 +9,33 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; import { EditorFrameInstance } from '../types'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { Document, SavedObjectStore } from '../persistence'; import { mount } from 'enzyme'; +import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public'; import { esFilters, FilterManager, IFieldType, IIndexPattern, -} from '../../../../../../src/plugins/data/public'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +} from '../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const dataStartMock = dataPluginMock.createStartContract(); -import { TopNavMenuData } from '../../../../../../src/plugins/navigation/public'; +import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks'; +import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { coreMock } from 'src/core/public/mocks'; -jest.mock('ui/new_platform'); jest.mock('../persistence'); jest.mock('src/core/public'); -import { npStart } from 'ui/new_platform'; -jest - .spyOn(npStart.plugins.navigation.ui.TopNavMenu.prototype, 'constructor') - .mockImplementation(() => { - return
; - }); +const navigationStartMock = navigationPluginMock.createStartContract(); + +jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { + return
; +}); -const { TopNavMenu } = npStart.plugins.navigation.ui; +const { TopNavMenu } = navigationStartMock.ui; function createMockFrame(): jest.Mocked { return { @@ -99,6 +99,7 @@ describe('Lens App', () => { function makeDefaultArgs(): jest.Mocked<{ editorFrame: EditorFrameInstance; data: typeof dataStartMock; + navigation: typeof navigationStartMock; core: typeof core; storage: Storage; docId?: string; @@ -107,6 +108,7 @@ describe('Lens App', () => { addToDashboardMode?: boolean; }> { return ({ + navigation: navigationStartMock, editorFrame: createMockFrame(), core: { ...core, @@ -140,6 +142,7 @@ describe('Lens App', () => { }, redirectTo: jest.fn(id => {}), } as unknown) as jest.Mocked<{ + navigation: typeof navigationStartMock; editorFrame: EditorFrameInstance; data: typeof dataStartMock; core: typeof core; @@ -338,6 +341,7 @@ describe('Lens App', () => { let defaultArgs: jest.Mocked<{ editorFrame: EditorFrameInstance; + navigation: typeof navigationStartMock; data: typeof dataStartMock; core: typeof core; storage: Storage; @@ -647,6 +651,27 @@ describe('Lens App', () => { }, }); }); + + it('does not show the copy button on first save', async () => { + const args = defaultArgs; + args.editorFrame = frame; + + instance = mount(); + + const onChange = frame.mount.mock.calls[0][1].onChange; + await act(async () => + onChange({ + filterableIndexPatterns: [], + doc: ({ expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + await act(async () => getButton(instance).run(instance.getDOMNode())); + instance.update(); + + expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); + }); }); }); @@ -654,6 +679,7 @@ describe('Lens App', () => { let defaultArgs: jest.Mocked<{ editorFrame: EditorFrameInstance; data: typeof dataStartMock; + navigation: typeof navigationStartMock; core: typeof core; storage: Storage; docId?: string; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx similarity index 96% rename from x-pack/legacy/plugins/lens/public/app_plugin/app.tsx rename to x-pack/plugins/lens/public/app_plugin/app.tsx index dfea2e39fcbc5..28135dd12a724 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -9,12 +9,12 @@ import React, { useState, useEffect, useCallback } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; -import { AppMountContext, NotificationsStart } from 'src/core/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { AppMountContext, NotificationsStart } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { npStart } from 'ui/new_platform'; import { FormattedMessage } from '@kbn/i18n/react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { SavedObjectSaveModal } from '../../../../../../src/plugins/saved_objects/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; @@ -25,7 +25,7 @@ import { IndexPattern as IndexPatternInstance, IndexPatternsContract, SavedQuery, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; interface State { isLoading: boolean; @@ -53,9 +53,11 @@ export function App({ docStorage, redirectTo, addToDashboardMode, + navigation, }: { editorFrame: EditorFrameInstance; data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; core: AppMountContext['core']; storage: IStorageWrapper; docId?: string; @@ -188,7 +190,7 @@ export function App({ [] ); - const { TopNavMenu } = npStart.plugins.navigation.ui; + const { TopNavMenu } = navigation.ui; const confirmButton = addToDashboardMode ? ( setState(s => ({ ...s, isSaveModalVisible: false }))} title={lastKnownDoc.title || ''} - showCopyOnSave={!addToDashboardMode} + showCopyOnSave={!!lastKnownDoc.id && !addToDashboardMode} objectType={i18n.translate('xpack.lens.app.saveModalType', { defaultMessage: 'Lens visualization', })} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts b/x-pack/plugins/lens/public/app_plugin/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/app_plugin/index.ts rename to x-pack/plugins/lens/public/app_plugin/index.ts diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_area.svg b/x-pack/plugins/lens/public/assets/chart_area.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_area.svg rename to x-pack/plugins/lens/public/assets/chart_area.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg b/x-pack/plugins/lens/public/assets/chart_area_stacked.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg rename to x-pack/plugins/lens/public/assets/chart_area_stacked.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar.svg b/x-pack/plugins/lens/public/assets/chart_bar.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar.svg rename to x-pack/plugins/lens/public/assets/chart_bar.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg b/x-pack/plugins/lens/public/assets/chart_bar_horizontal.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg rename to x-pack/plugins/lens/public/assets/chart_bar_horizontal.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg b/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg rename to x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg b/x-pack/plugins/lens/public/assets/chart_bar_stacked.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg rename to x-pack/plugins/lens/public/assets/chart_bar_stacked.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg b/x-pack/plugins/lens/public/assets/chart_datatable.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg rename to x-pack/plugins/lens/public/assets/chart_datatable.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_line.svg b/x-pack/plugins/lens/public/assets/chart_line.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_line.svg rename to x-pack/plugins/lens/public/assets/chart_line.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_metric.svg b/x-pack/plugins/lens/public/assets/chart_metric.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_metric.svg rename to x-pack/plugins/lens/public/assets/chart_metric.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg b/x-pack/plugins/lens/public/assets/chart_mixed_xy.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg rename to x-pack/plugins/lens/public/assets/chart_mixed_xy.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_dark_2x.png b/x-pack/plugins/lens/public/assets/lens_app_graphic_dark_2x.png similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_dark_2x.png rename to x-pack/plugins/lens/public/assets/lens_app_graphic_dark_2x.png diff --git a/x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_light_2x.png b/x-pack/plugins/lens/public/assets/lens_app_graphic_light_2x.png similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_light_2x.png rename to x-pack/plugins/lens/public/assets/lens_app_graphic_light_2x.png diff --git a/x-pack/plugins/lens/public/datatable_visualization/_index.scss b/x-pack/plugins/lens/public/datatable_visualization/_index.scss new file mode 100644 index 0000000000000..532e8106b023f --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/_index.scss @@ -0,0 +1 @@ +@import 'visualization'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss rename to x-pack/plugins/lens/public/datatable_visualization/_visualization.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx rename to x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 050f294d0b964..cbdc0617e8568 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -13,7 +13,7 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, IInterpreterRenderHandlers, -} from '../../../../../../src/plugins/expressions/public'; +} from '../../../../../src/plugins/expressions/public'; import { VisualizationContainer } from '../visualization_container'; export interface DatatableColumns { diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts similarity index 89% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts rename to x-pack/plugins/lens/public/datatable_visualization/index.ts index 7a54a5daa3095..ff036aadfd4cf 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/public'; +import { CoreSetup } from 'kibana/public'; import { datatableVisualization } from './visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { datatable, datatableColumns, getDatatableRenderer } from './expression'; import { EditorFrameSetup, FormatFactory } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx rename to x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx rename to x-pack/plugins/lens/public/datatable_visualization/visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.test.tsx b/x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.test.tsx rename to x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.tsx b/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.tsx rename to x-pack/plugins/lens/public/debounced_component/debounced_component.tsx diff --git a/x-pack/legacy/plugins/lens/public/debounced_component/index.ts b/x-pack/plugins/lens/public/debounced_component/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/debounced_component/index.ts rename to x-pack/plugins/lens/public/debounced_component/index.ts diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap rename to x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/_drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/_drag_drop.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/_drag_drop.scss rename to x-pack/plugins/lens/public/drag_drop/_drag_drop.scss diff --git a/x-pack/plugins/lens/public/drag_drop/_index.scss b/x-pack/plugins/lens/public/drag_drop/_index.scss new file mode 100644 index 0000000000000..ddf9b9aa3e429 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/_index.scss @@ -0,0 +1 @@ +@import 'drag_drop'; diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx rename to x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.tsx rename to x-pack/plugins/lens/public/drag_drop/drag_drop.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/index.ts b/x-pack/plugins/lens/public/drag_drop/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/index.ts rename to x-pack/plugins/lens/public/drag_drop/index.ts diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/providers.test.tsx b/x-pack/plugins/lens/public/drag_drop/providers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/providers.test.tsx rename to x-pack/plugins/lens/public/drag_drop/providers.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/providers.tsx rename to x-pack/plugins/lens/public/drag_drop/providers.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/readme.md b/x-pack/plugins/lens/public/drag_drop/readme.md similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/readme.md rename to x-pack/plugins/lens/public/drag_drop/readme.md diff --git a/x-pack/plugins/lens/public/editor_frame_service/_index.scss b/x-pack/plugins/lens/public/editor_frame_service/_index.scss new file mode 100644 index 0000000000000..199cbe35e25fa --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/_index.scss @@ -0,0 +1 @@ +@import 'editor_frame/index'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 2a9499077f3c1..cd0aee732793e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -7,12 +7,11 @@ import React, { useMemo, memo, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { DatasourceDataPanelProps, Datasource } from '../../../public'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; import { DragContext } from '../../drag_drop'; -import { StateSetter, FramePublicAPI } from '../../types'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; interface DataPanelWrapperProps { datasourceState: unknown; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx diff --git a/x-pack/legacy/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 similarity index 97% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 082519d9a8feb..b72d9081bbc91 100644 --- a/x-pack/legacy/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 @@ -5,8 +5,8 @@ */ import React, { useEffect, useReducer } from 'react'; -import { CoreSetup, CoreStart } from 'src/core/public'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { Datasource, DatasourcePublicAPI, @@ -25,7 +25,7 @@ import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; -import { Filter, Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; +import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; export interface EditorFrameProps { doc?: Document; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts similarity index 97% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index d264e6d0da3ad..9d4f8587577a3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -6,7 +6,7 @@ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { Visualization, Datasource, FramePublicAPI } from '../../types'; -import { Filter, TimeRange, Query } from '../../../../../../../src/plugins/data/public'; +import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss new file mode 100644 index 0000000000000..d4b27c6c98b3c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -0,0 +1,8 @@ +@import 'chart_switch'; +@import 'config_panel_wrapper'; +@import 'data_panel_wrapper'; +@import 'expression_renderer'; +@import 'frame_layout'; +@import 'suggestion_panel'; +@import 'workspace_panel_wrapper'; + diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index 60bfbc493f61c..9c7ed265f3539 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -6,7 +6,7 @@ import { getSavedObjectFormat, Props } from './save'; import { createMockDatasource, createMockVisualization } from '../mocks'; -import { esFilters, IIndexPattern, IFieldType } from '../../../../../../../src/plugins/data/public'; +import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public'; describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 4aaf2a3ee9e81..1f62929783b63 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -5,7 +5,7 @@ */ import { getInitialState, reducer } from './state_management'; -import { EditorFrameProps } from '.'; +import { EditorFrameProps } from './index'; import { Datasource, Visualization } from '../../types'; import { createExpressionRendererMock } from '../mocks'; import { coreMock } from 'src/core/public/mocks'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index 7d763bcac2cc9..bb6daf5641a64 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EditorFrameProps } from '../editor_frame'; +import { EditorFrameProps } from './index'; import { Document } from '../../persistence/saved_object_store'; export interface PreviewState { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 0e32f1f053b9d..240bdff40b51c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -15,8 +15,8 @@ import { createMockFramePublicAPI, } from '../mocks'; import { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; -import { esFilters, IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; +import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public'; import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; import { getSuggestions, Suggestion } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 76443027ab88a..867214d15578a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -24,7 +24,7 @@ import classNames from 'classnames'; import { Action, PreviewState } from './state_management'; import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index 748e5b876da95..33ecee53fa3bc 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererProps } from '../../../../../../src/plugins/expressions/public'; import { FramePublicAPI, TableSuggestion, Visualization } from '../../types'; import { createMockVisualization, @@ -21,7 +21,7 @@ import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { Ast } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; -import { esFilters, IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public'; describe('workspace_panel', () => { let mockVisualization: jest.Mocked; diff --git a/x-pack/legacy/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 similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index c2a5c16e405a2..1f741ca37934f 100644 --- a/x-pack/legacy/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 @@ -16,8 +16,8 @@ import { EuiBetaBadge, EuiButtonEmpty, } from '@elastic/eui'; -import { CoreStart, CoreSetup } from 'src/core/public'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +import { CoreStart, CoreSetup } from 'kibana/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { Action } from './state_management'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; import { DragDrop, DragContext } from '../../drag_drop'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 55363ebe4d8f3..aeae64514b0fd 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -9,9 +9,9 @@ import { Embeddable } from './embeddable'; import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; import { Query, TimeRange, Filter, TimefilterContract } from 'src/plugins/data/public'; import { Document } from '../../persistence'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; -jest.mock('../../../../../../../src/plugins/inspector/public/', () => ({ +jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, open: false, })); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index c2ab1c72af545..0ef5f6d1a5470 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -16,15 +16,15 @@ import { } from 'src/plugins/data/public'; import { Subscription } from 'rxjs'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; import { Embeddable as AbstractEmbeddable, EmbeddableOutput, IContainer, EmbeddableInput, -} from '../../../../../../../src/plugins/embeddable/public'; +} from '../../../../../../src/plugins/embeddable/public'; import { Document, DOC_TYPE } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts similarity index 91% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 99a59c756e228..68dbff263f60d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -15,17 +15,17 @@ import { IndexPatternsContract, IndexPattern, TimefilterContract, -} from '../../../../../../../src/plugins/data/public'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +} from '../../../../../../src/plugins/data/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { EmbeddableFactoryDefinition, ErrorEmbeddable, EmbeddableInput, IContainer, -} from '../../../../../../../src/plugins/embeddable/public'; +} from '../../../../../../src/plugins/embeddable/public'; import { Embeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; -import { getEditPath } from '../../../../../../plugins/lens/common'; +import { getEditPath } from '../../../common'; interface StartServices { timefilter: TimefilterContract; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/format_column.ts b/x-pack/plugins/lens/public/editor_frame_service/format_column.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/format_column.ts rename to x-pack/plugins/lens/public/editor_frame_service/format_column.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts b/x-pack/plugins/lens/public/editor_frame_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts rename to x-pack/plugins/lens/public/editor_frame_service/index.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts index 9368674de31c5..243441f2c8ab3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts @@ -8,8 +8,6 @@ import moment from 'moment'; import { mergeTables } from './merge_tables'; import { KibanaDatatable } from 'src/plugins/expressions'; -jest.mock('ui/new_platform'); - describe('lens_merge_tables', () => { it('should produce a row with the nested table as defined', () => { const sampleTable1: KibanaDatatable = { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts rename to x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts index c06640fb25de6..7c10ee4a57fad 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts @@ -10,7 +10,7 @@ import { ExpressionValueSearchContext, KibanaDatatable, } from 'src/plugins/expressions/public'; -import { search } from '../../../../../../src/plugins/data/public'; +import { search } from '../../../../../src/plugins/data/public'; const { toAbsoluteDates } = search.aggs; import { LensMultiTable } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx similarity index 92% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx rename to x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 5d2f68a5567eb..50cd1ad8bd53a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -9,12 +9,12 @@ import { ReactExpressionRendererProps, ExpressionsSetup, ExpressionsStart, -} from '../../../../../../src/plugins/expressions/public'; -import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; -import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; +} from '../../../../../src/plugins/expressions/public'; +import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks'; +import { expressionsPluginMock } from '../../../../../src/plugins/expressions/public/mocks'; import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types'; import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; export function createMockVisualization(): jest.Mocked { return { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/service.test.tsx index 42a1fcc055a1e..fbd65c5044d51 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx @@ -14,8 +14,6 @@ import { } from './mocks'; import { CoreSetup } from 'kibana/public'; -jest.mock('ui/new_platform'); - // mock away actual dependencies to prevent all of it being loaded jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx similarity index 91% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx rename to x-pack/plugins/lens/public/editor_frame_service/service.tsx index 1375c60060ca8..15fe449d6563b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -7,16 +7,13 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreSetup, CoreStart } from 'src/core/public'; -import { - ExpressionsSetup, - ExpressionsStart, -} from '../../../../../../src/plugins/expressions/public'; -import { EmbeddableSetup, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; import { Datasource, Visualization, @@ -32,13 +29,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; - embeddable: EmbeddableSetup; + embeddable?: EmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: DataPublicPluginStart; - embeddable: EmbeddableStart; + embeddable?: EmbeddableStart; expressions: ExpressionsStart; } @@ -79,7 +76,9 @@ export class EditorFrameService { }; }; - plugins.embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices)); + if (plugins.embeddable) { + plugins.embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices)); + } return { registerDatasource: datasource => { diff --git a/x-pack/legacy/plugins/lens/public/help_menu_util.tsx b/x-pack/plugins/lens/public/help_menu_util.tsx similarity index 63% rename from x-pack/legacy/plugins/lens/public/help_menu_util.tsx rename to x-pack/plugins/lens/public/help_menu_util.tsx index 9ead31690e854..333a90df4731b 100644 --- a/x-pack/legacy/plugins/lens/public/help_menu_util.tsx +++ b/x-pack/plugins/lens/public/help_menu_util.tsx @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { ChromeStart } from 'kibana/public'; +import { ChromeStart, DocLinksStart } from 'kibana/public'; -export function addHelpMenuToAppChrome(chrome: ChromeStart) { +export function addHelpMenuToAppChrome(chrome: ChromeStart, docLinks: DocLinksStart) { chrome.setHelpExtension({ appName: 'Lens', links: [ { linkType: 'documentation', - href: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, + href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/lens.html`, }, { linkType: 'github', diff --git a/x-pack/legacy/plugins/lens/public/helpers/index.ts b/x-pack/plugins/lens/public/helpers/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/helpers/index.ts rename to x-pack/plugins/lens/public/helpers/index.ts diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts b/x-pack/plugins/lens/public/helpers/url_helper.test.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts rename to x-pack/plugins/lens/public/helpers/url_helper.test.ts index ef960fb52952b..37e35ca17e0b3 100644 --- a/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts +++ b/x-pack/plugins/lens/public/helpers/url_helper.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../../../src/plugins/dashboard/public', () => ({ +jest.mock('../../../../../src/plugins/dashboard/public', () => ({ DashboardConstants: { ADD_EMBEDDABLE_ID: 'addEmbeddableId', ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts b/x-pack/plugins/lens/public/helpers/url_helper.ts similarity index 95% rename from x-pack/legacy/plugins/lens/public/helpers/url_helper.ts rename to x-pack/plugins/lens/public/helpers/url_helper.ts index 3495c15118ce7..0a97ba4b2edf7 100644 --- a/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts +++ b/x-pack/plugins/lens/public/helpers/url_helper.ts @@ -5,7 +5,7 @@ */ import { parseUrl, stringify } from 'query-string'; -import { DashboardConstants } from '../../../../../../src/plugins/dashboard/public'; +import { DashboardConstants } from '../../../../../src/plugins/dashboard/public'; type UrlVars = Record; diff --git a/x-pack/legacy/plugins/lens/public/id_generator/id_generator.test.ts b/x-pack/plugins/lens/public/id_generator/id_generator.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/id_generator/id_generator.test.ts rename to x-pack/plugins/lens/public/id_generator/id_generator.test.ts diff --git a/x-pack/legacy/plugins/lens/public/id_generator/id_generator.ts b/x-pack/plugins/lens/public/id_generator/id_generator.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/id_generator/id_generator.ts rename to x-pack/plugins/lens/public/id_generator/id_generator.ts diff --git a/x-pack/legacy/plugins/lens/public/id_generator/index.ts b/x-pack/plugins/lens/public/id_generator/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/id_generator/index.ts rename to x-pack/plugins/lens/public/id_generator/index.ts diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/plugins/lens/public/index.scss similarity index 54% rename from x-pack/legacy/plugins/lens/public/index.scss rename to x-pack/plugins/lens/public/index.scss index 2f91d14c397c7..67bbac12be8c3 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/plugins/lens/public/index.scss @@ -1,12 +1,13 @@ // Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; +@import '@elastic/eui/src/global_styling/variables/index'; +@import '@elastic/eui/src/global_styling/mixins/index'; -@import './variables'; -@import './mixins'; +@import 'variables'; +@import 'mixins'; -@import './app_plugin/index'; +@import 'app_plugin/index'; @import 'datatable_visualization/index'; -@import './drag_drop/index'; +@import 'drag_drop/index'; @import 'editor_frame_service/index'; @import 'indexpattern_datasource/index'; @import 'xy_visualization/index'; diff --git a/x-pack/legacy/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/index.ts rename to x-pack/plugins/lens/public/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap rename to x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss rename to x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss rename to x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss index 89f6bbf908419..41919b900c71f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss @@ -14,7 +14,7 @@ } .lnsFieldItem--missing { - background: lightOrDarkTheme(transparentize($euiColorMediumShade, 0.9), $euiColorEmptyShade); + background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade); color: $euiColorDarkShade; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss new file mode 100644 index 0000000000000..e5d8b408e33e5 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss @@ -0,0 +1,4 @@ +@import 'datapanel'; +@import 'field_item'; + +@import 'dimension_panel/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts index cc1a74a1854ce..5f35ef650a08c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { getAutoDate } from './auto_date'; describe('auto_date', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts similarity index 92% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts index 063cbb4d217a7..97a46f4a3e176 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataPublicPluginSetup } from '../../../../../../src/plugins/data/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { ExpressionFunctionDefinition, KibanaContext, -} from '../../../../../../src/plugins/expressions/public'; +} from '../../../../../src/plugins/expressions/public'; interface LensAutoDateProps { aggConfigs: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 3066ac0e11325..c396f0efee42e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -6,6 +6,7 @@ import React, { ChangeEvent } from 'react'; import { createMockedDragDropContext } from './mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel'; import { FieldItem } from './field_item'; import { act } from 'react-dom/test-utils'; @@ -16,8 +17,6 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress } from '@elastic/eui'; import { documentField } from './document_field'; -jest.mock('ui/new_platform'); - const initialState: IndexPatternPrivateState = { indexPatternRefs: [], existingFields: {}, @@ -218,6 +217,7 @@ describe('IndexPattern Data Panel', () => { defaultProps = { indexPatternRefs: [], existingFields: {}, + data: dataPluginMock.createStartContract(), dragDropContext: createMockedDragDropContext(), currentIndexPatternId: '1', indexPatterns: initialState.indexPatterns, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 7a3c04b67fbc4..79dcdafd916b4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -27,6 +27,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; import { FieldItem } from './field_item'; @@ -40,9 +41,10 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; -import { esQuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public'; export type Props = DatasourceDataPanelProps & { + data: DataPublicPluginStart; changeIndexPattern: ( id: string, state: IndexPatternPrivateState, @@ -78,6 +80,7 @@ export function IndexPatternDataPanel({ state, dragDropContext, core, + data, query, filters, dateRange, @@ -152,6 +155,7 @@ export function IndexPatternDataPanel({ showEmptyFields={state.showEmptyFields} onToggleEmptyFields={onToggleEmptyFields} core={core} + data={data} onChangeIndexPattern={onChangeIndexPattern} existingFields={state.existingFields} /> @@ -177,8 +181,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ showEmptyFields, onToggleEmptyFields, core, + data, existingFields, }: Pick> & { + data: DataPublicPluginStart; currentIndexPatternId: string; indexPatternRefs: IndexPatternRef[]; indexPatterns: Record; @@ -441,6 +447,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ {specialFields.map(field => ( { let defaultProps: FieldItemProps; let indexPattern: IndexPattern; let core: ReturnType; + let data: DataPublicPluginStart; beforeEach(() => { indexPattern = { @@ -61,9 +60,11 @@ describe('IndexPattern Field Item', () => { } as IndexPattern; core = coreMock.createSetup(); + data = dataPluginMock.createStartContract(); core.http.post.mockClear(); defaultProps = { indexPattern, + data, core, highlight: '', dateRange: { @@ -81,7 +82,7 @@ describe('IndexPattern Field Item', () => { exists: true, }; - npStart.plugins.data.fieldFormats = ({ + data.fieldFormats = ({ getDefaultInstance: jest.fn(() => ({ convert: jest.fn((s: unknown) => JSON.stringify(s)), })), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index b98f589bc5b98..c4d2a6f8780c6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -20,7 +20,6 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; -import { npStart } from 'ui/new_platform'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { Axis, @@ -33,6 +32,7 @@ import { TooltipType, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { Query, KBN_FIELD_TYPES, @@ -40,17 +40,18 @@ import { Filter, esQuery, IIndexPattern, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; import { DraggedField } from './indexpattern'; import { DragDrop } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; -import { BucketedAggregation, FieldStatsResponse } from '../../../../../plugins/lens/common'; +import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField } from './types'; import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; export interface FieldItemProps { core: DatasourceDataPanelProps['core']; + data: DataPublicPluginStart; field: IndexPatternField; indexPattern: IndexPattern; highlight?: string; @@ -237,8 +238,16 @@ export function FieldItem(props: FieldItemProps) { } function FieldItemPopoverContents(props: State & FieldItemProps) { - const fieldFormats = npStart.plugins.data.fieldFormats; - const { histogram, topValues, indexPattern, field, dateRange, core, sampledValues } = props; + const { + histogram, + topValues, + indexPattern, + field, + dateRange, + core, + sampledValues, + data: { fieldFormats }, + } = props; const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts similarity index 84% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 8a5c562ebd455..fe14f472341af 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/public'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { CoreSetup } from 'kibana/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; import { getAutoDate } from './auto_date'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { DataPublicPluginSetup, DataPublicPluginStart, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; import { Datasource, EditorFrameSetup } from '../types'; export interface IndexPatternDatasourceSetupPlugins { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 76e59a170a9e9..dbdbe4e3f9442 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -8,13 +8,11 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { getIndexPatternDatasource, IndexPatternColumn, uniqueLabels } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; -import { pluginsMock } from 'ui/new_platform/__mocks__/helpers'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; jest.mock('./loader'); jest.mock('../id_generator'); -// Contains old and new platform data plugins, used for interpreter and filter ratio -jest.mock('ui/new_platform'); const expectedIndexPatterns = { 1: { @@ -140,7 +138,7 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource = getIndexPatternDatasource({ storage: {} as IStorageWrapper, core: coreMock.createStart(), - data: pluginsMock.createStart().data, + data: dataPluginMock.createStartContract(), }); persistedState = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 9c2a9c9bf4a09..b8f0460f2a9ab 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'src/core/public'; +import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -42,10 +42,10 @@ import { IndexPatternPrivateState, IndexPatternPersistedState, } from './types'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { Plugin as DataPlugin } from '../../../../../../src/plugins/data/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { deleteColumn } from './state_helpers'; -import { Datasource, StateSetter } from '..'; +import { Datasource, StateSetter } from '../index'; export { OperationType, IndexPatternColumn } from './operations'; @@ -105,7 +105,7 @@ export function getIndexPatternDatasource({ }: { core: CoreStart; storage: IStorageWrapper; - data: ReturnType; + data: DataPublicPluginStart; }) { const savedObjectsClient = core.savedObjects.client; const uiSettings = core.uiSettings; @@ -209,6 +209,7 @@ export function getIndexPatternDatasource({ onError: onIndexPatternLoadError, }); }} + data={data} {...props} /> , diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index fe14e5de5c1e3..2008b326a539c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -12,7 +12,6 @@ import { getDatasourceSuggestionsFromCurrentState, } from './indexpattern_suggestions'; -jest.mock('ui/new_platform'); jest.mock('./loader'); jest.mock('../id_generator'); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 219a6d935e436..4dd29d7925916 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -12,7 +12,6 @@ import { ShallowWrapper } from 'enzyme'; import { EuiSelectable, EuiSelectableList } from '@elastic/eui'; import { ChangeIndexPattern } from './change_indexpattern'; -jest.mock('ui/new_platform'); jest.mock('./state_helpers'); const initialState: IndexPatternPrivateState = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx similarity index 86% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx index 06eda73748cef..bcc83e799d889 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { FieldIcon, FieldIconProps } from '../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon, FieldIconProps } from '../../../../../src/plugins/kibana_react/public'; import { DataType } from '../types'; import { normalizeOperationDataType } from './utils'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index ea9c8213ba909..cacf729ba0caf 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -16,8 +16,6 @@ import { import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { documentField } from './document_field'; -// TODO: This should not be necessary -jest.mock('ui/new_platform'); jest.mock('./operations'); const sampleIndexPatterns = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index f4d5857f4826d..23faab768eba6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,8 +5,8 @@ */ import _ from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'src/core/public'; -import { SimpleSavedObject } from 'src/core/public'; +import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'kibana/public'; +import { SimpleSavedObject } from 'kibana/public'; import { StateSetter } from '../types'; import { IndexPattern, @@ -16,14 +16,14 @@ import { IndexPatternField, } from './types'; import { updateLayerIndexPattern } from './state_helpers'; -import { DateRange, ExistingFields } from '../../../../../plugins/lens/common/types'; -import { BASE_API_URL } from '../../../../../plugins/lens/common'; +import { DateRange, ExistingFields } from '../../common/types'; +import { BASE_API_URL } from '../../common'; import { documentField } from './document_field'; import { indexPatterns as indexPatternsUtils, IFieldType, IndexPatternTypeMeta, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; interface SavedIndexPatternAttributes extends SavedObjectAttributes { title: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 33325016deaeb..9491ca9ea3787 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn } from './column_types'; const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 1592b1049f666..1dcaf78b58a6c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn } from './column_types'; import { IndexPatternField } from '../../types'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index dc279fca82d4b..e3b6061248f3b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -6,21 +6,19 @@ import React from 'react'; import { DateHistogramIndexPatternColumn } from './date_histogram'; -import { dateHistogramOperation } from '.'; +import { dateHistogramOperation } from './index'; import { shallow } from 'enzyme'; import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { coreMock } from 'src/core/public/mocks'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { dataPluginMock, getCalculateAutoTimeExpression, -} from '../../../../../../../../src/plugins/data/public/mocks'; +} from '../../../../../../../src/plugins/data/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; -jest.mock('ui/new_platform'); - const dataStart = dataPluginMock.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression({ ...coreMock.createStart().uiSettings, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 452d5c9140868..7a36d52ad897b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -21,12 +21,9 @@ import { EuiSpacer, } from '@elastic/eui'; import { updateColumnParam } from '../../state_helpers'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FieldBasedIndexPatternColumn } from './column_types'; -import { - IndexPatternAggRestrictions, - search, -} from '../../../../../../../../src/plugins/data/public'; +import { IndexPatternAggRestrictions, search } from '../../../../../../../src/plugins/data/public'; const { isValidInterval } = search.aggs; const autoInterval = 'auto'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 2db5296905000..ef12fca690f0c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { termsOperation } from './terms'; import { cardinalityOperation } from './cardinality'; @@ -14,8 +14,8 @@ import { countOperation } from './count'; import { DimensionPriority, StateSetter, OperationMetadata } from '../../../types'; import { BaseIndexPatternColumn } from './column_types'; import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../types'; -import { DateRange } from '../../../../../../../plugins/lens/common'; -import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; +import { DateRange } from '../../../../common'; +import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; // List of all operation definitions registered to this data source. // If you want to implement a new operation, add it to this array and diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index c2d9478c6ea15..3da635dc13d10 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn } from './column_types'; type MetricColumn = FormattedIndexPatternColumn & { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index fc0c9746b2f98..8f6130e74b5b8 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -7,16 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiRange, EuiSelect } from '@elastic/eui'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { TermsIndexPatternColumn } from './terms'; -import { termsOperation } from '.'; +import { termsOperation } from './index'; import { IndexPatternPrivateState } from '../../types'; -jest.mock('ui/new_platform'); - const defaultProps = { storage: {} as IStorageWrapper, uiSettings: {} as IUiSettingsClient, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index 387b197c9235c..29e5787fa4f54 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -10,7 +10,7 @@ import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; import { IndexPatternColumn } from '../../indexpattern'; import { updateColumnParam } from '../../state_helpers'; import { DataType } from '../../../types'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FieldBasedIndexPatternColumn } from './column_types'; type PropType = C extends React.ComponentType ? P : unknown; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 3602491c6eb2c..111b1040de989 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColumn } from '.'; +import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColumn } from './index'; import { AvgIndexPatternColumn, MinIndexPatternColumn } from './definitions/metrics'; import { CountIndexPatternColumn } from './definitions/count'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; -jest.mock('ui/new_platform'); jest.mock('../loader'); const expectedIndexPatterns = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts index 9da7591305a6c..4bfd6a4f93c75 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts @@ -5,8 +5,8 @@ */ import { renameColumns } from './rename_columns'; -import { KibanaDatatable } from '../../../../../../src/plugins/expressions/public'; -import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; +import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; describe('rename_columns', () => { it('should rename columns of a given datatable', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index 0a58853f1ef4f..1e3251a8dedd8 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -17,7 +17,6 @@ import { DateHistogramIndexPatternColumn } from './operations/definitions/date_h import { AvgIndexPatternColumn } from './operations/definitions/metrics'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; -jest.mock('ui/new_platform'); jest.mock('./operations'); describe('state_helpers', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts similarity index 94% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 3820ff3b387bb..563af40ed2720 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -5,7 +5,7 @@ */ import { IndexPatternColumn } from './operations'; -import { IndexPatternAggRestrictions } from '../../../../../../src/plugins/data/public'; +import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public'; export interface IndexPattern { id: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/utils.ts diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts rename to x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts rename to x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts index 73750a65c50b8..10b052c66efed 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts @@ -5,10 +5,10 @@ */ import moment from 'moment'; -import { HttpSetup } from 'src/core/public'; +import { HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { BASE_API_URL } from '../../../../../plugins/lens/common'; +import { BASE_API_URL } from '../../common'; const STORAGE_KEY = 'lens-ui-telemetry'; diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts rename to x-pack/plugins/lens/public/lens_ui_telemetry/index.ts diff --git a/x-pack/legacy/plugins/lens/public/loader.test.tsx b/x-pack/plugins/lens/public/loader.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/loader.test.tsx rename to x-pack/plugins/lens/public/loader.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/loader.tsx b/x-pack/plugins/lens/public/loader.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/loader.tsx rename to x-pack/plugins/lens/public/loader.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx b/x-pack/plugins/lens/public/metric_visualization/auto_scale.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx rename to x-pack/plugins/lens/public/metric_visualization/auto_scale.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx b/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx rename to x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/index.scss b/x-pack/plugins/lens/public/metric_visualization/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/index.scss rename to x-pack/plugins/lens/public/metric_visualization/index.scss diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts b/x-pack/plugins/lens/public/metric_visualization/index.ts similarity index 88% rename from x-pack/legacy/plugins/lens/public/metric_visualization/index.ts rename to x-pack/plugins/lens/public/metric_visualization/index.ts index 65f064258a5e2..2960da52191e4 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts +++ b/x-pack/plugins/lens/public/metric_visualization/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/public'; +import { CoreSetup } from 'kibana/public'; import { metricVisualization } from './metric_visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { metricChart, getMetricChartRenderer } from './metric_expression'; import { EditorFrameSetup, FormatFactory } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_expression.test.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx rename to x-pack/plugins/lens/public/metric_visualization/metric_expression.test.tsx index 3da38d486aecd..2406e7cd42ebc 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_expression.test.tsx @@ -9,8 +9,8 @@ import { LensMultiTable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; import { MetricConfig } from './types'; -import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; -import { IFieldFormat } from '../../../../../../src/plugins/data/public'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; +import { IFieldFormat } from '../../../../../src/plugins/data/public'; function sampleArgs() { const data: LensMultiTable = { diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_expression.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx rename to x-pack/plugins/lens/public/metric_visualization/metric_expression.tsx index a80552e57a9e0..3484837f65b43 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_expression.tsx @@ -10,7 +10,7 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, IInterpreterRenderHandlers, -} from '../../../../../../src/plugins/expressions/public'; +} from '../../../../../src/plugins/expressions/public'; import { MetricConfig } from './types'; import { FormatFactory, LensMultiTable } from '../types'; import { AutoScale } from './auto_scale'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts similarity index 98% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts rename to x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts index c9bfadbefaf5f..ef93f0b5bf064 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts @@ -5,7 +5,7 @@ */ import { getSuggestions } from './metric_suggestions'; -import { TableSuggestionColumn, TableSuggestion } from '..'; +import { TableSuggestionColumn, TableSuggestion } from '../index'; describe('metric_suggestions', () => { function numCol(columnId: string): TableSuggestionColumn { diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts rename to x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts rename to x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx rename to x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts b/x-pack/plugins/lens/public/metric_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/types.ts rename to x-pack/plugins/lens/public/metric_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/native_renderer/index.ts b/x-pack/plugins/lens/public/native_renderer/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/native_renderer/index.ts rename to x-pack/plugins/lens/public/native_renderer/index.ts diff --git a/x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.test.tsx b/x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.test.tsx rename to x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.tsx b/x-pack/plugins/lens/public/native_renderer/native_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.tsx rename to x-pack/plugins/lens/public/native_renderer/native_renderer.tsx diff --git a/x-pack/legacy/plugins/lens/public/persistence/index.ts b/x-pack/plugins/lens/public/persistence/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/persistence/index.ts rename to x-pack/plugins/lens/public/persistence/index.ts diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts rename to x-pack/plugins/lens/public/persistence/saved_object_store.test.ts diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts similarity index 94% rename from x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts rename to x-pack/plugins/lens/public/persistence/saved_object_store.ts index ac0b3322b400e..015f4b9b825f4 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -5,8 +5,8 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectAttributes } from 'src/core/server'; -import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { SavedObjectAttributes } from 'kibana/server'; +import { Query, Filter } from '../../../../../src/plugins/data/public'; export interface Document { id?: string; diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/plugins/lens/public/plugin.tsx similarity index 90% rename from x-pack/legacy/plugins/lens/public/plugin.tsx rename to x-pack/plugins/lens/public/plugin.tsx index b426a12d07f9b..8d760eb0df501 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/plugins/lens/public/plugin.tsx @@ -11,14 +11,15 @@ import { render, unmountComponentAtNode } from 'react-dom'; import rison, { RisonObject, RisonValue } from 'rison-node'; import { isObject } from 'lodash'; -import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; +import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; import { VisualizationsSetup } from 'src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; -import { DashboardConstants } from '../../../../../src/plugins/dashboard/public'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { DashboardConstants } from '../../../../src/plugins/dashboard/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { EditorFrameService } from './editor_frame_service'; import { IndexPatternDatasource } from './indexpattern_datasource'; import { addHelpMenuToAppChrome } from './help_menu_util'; @@ -34,17 +35,19 @@ import { trackUiEvent, } from './lens_ui_telemetry'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../plugins/lens/common'; +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { addEmbeddableToDashboardUrl, getUrlVars } from './helpers'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; +import './index.scss'; + export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; expressions: ExpressionsSetup; data: DataPublicPluginSetup; - embeddable: EmbeddableSetup; + embeddable?: EmbeddableSetup; visualizations: VisualizationsSetup; } @@ -52,6 +55,7 @@ export interface LensPluginStartDependencies { data: DataPublicPluginStart; embeddable: EmbeddableStart; expressions: ExpressionsStart; + navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; } @@ -75,7 +79,7 @@ export class LensPlugin { } setup( - core: CoreSetup, + core: CoreSetup, { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies ) { const editorFrameSetupInterface = this.editorFrameService.setup(core, { @@ -103,9 +107,9 @@ export class LensPlugin { title: NOT_INTERNATIONALIZED_PRODUCT_NAME, mount: async (params: AppMountParameters) => { const [coreStart, startDependencies] = await core.getStartServices(); - const dataStart = startDependencies.data; + const { data: dataStart, navigation } = startDependencies; const savedObjectsClient = coreStart.savedObjects.client; - addHelpMenuToAppChrome(coreStart.chrome); + addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); const instance = await this.createEditorFrame!(); @@ -157,6 +161,7 @@ export class LensPlugin { void; diff --git a/x-pack/legacy/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts similarity index 95% rename from x-pack/legacy/plugins/lens/public/vis_type_alias.ts rename to x-pack/plugins/lens/public/vis_type_alias.ts index 123b994e6ccce..807504ee2b9c2 100644 --- a/x-pack/legacy/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { VisTypeAlias } from 'src/plugins/visualizations/public'; -import { getBasePath, getEditPath } from '../../../../plugins/lens/common'; +import { getBasePath, getEditPath } from '../common'; export const getLensAliasConfig = (): VisTypeAlias => ({ aliasUrl: getBasePath(), diff --git a/x-pack/legacy/plugins/lens/public/visualization_container.test.tsx b/x-pack/plugins/lens/public/visualization_container.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/visualization_container.test.tsx rename to x-pack/plugins/lens/public/visualization_container.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/visualization_container.tsx b/x-pack/plugins/lens/public/visualization_container.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/visualization_container.tsx rename to x-pack/plugins/lens/public/visualization_container.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap rename to x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap rename to x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap diff --git a/x-pack/plugins/lens/public/xy_visualization/_index.scss b/x-pack/plugins/lens/public/xy_visualization/_index.scss new file mode 100644 index 0000000000000..110a9589a6fb4 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/_index.scss @@ -0,0 +1 @@ +@import 'xy_expression'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss b/x-pack/plugins/lens/public/xy_visualization/_xy_expression.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss rename to x-pack/plugins/lens/public/xy_visualization/_xy_expression.scss diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts similarity index 89% rename from x-pack/legacy/plugins/lens/public/xy_visualization/index.ts rename to x-pack/plugins/lens/public/xy_visualization/index.ts index 8cc5abb44d6e1..5dfae097be834 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -5,14 +5,14 @@ */ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { CoreSetup, IUiSettingsClient, CoreStart } from 'src/core/public'; +import { CoreSetup, IUiSettingsClient, CoreStart } from 'kibana/public'; import moment from 'moment-timezone'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; import { EditorFrameSetup, FormatFactory } from '../types'; -import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { setExecuteTriggerActions } from './services'; export interface XyVisualizationPluginSetupPlugins { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/services.ts b/x-pack/plugins/lens/public/xy_visualization/services.ts similarity index 70% rename from x-pack/legacy/plugins/lens/public/xy_visualization/services.ts rename to x-pack/plugins/lens/public/xy_visualization/services.ts index af683efb86534..51289fe0c63e7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/services.ts +++ b/x-pack/plugins/lens/public/xy_visualization/services.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public'; -import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; +import { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter< UiActionsStart['executeTriggerActions'] diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts rename to x-pack/plugins/lens/public/xy_visualization/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts rename to x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts rename to x-pack/plugins/lens/public/xy_visualization/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/xy_visualization/types.ts rename to x-pack/plugins/lens/public/xy_visualization/types.ts index f7b4afc76ec4b..7a5837d382c7b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -15,7 +15,7 @@ import chartBarHorizontalSVG from '../assets/chart_bar_horizontal.svg'; import chartBarHorizontalStackedSVG from '../assets/chart_bar_horizontal_stacked.svg'; import chartLineSVG from '../assets/chart_line.svg'; -import { VisualizationType } from '..'; +import { VisualizationType } from '../index'; export interface LegendConfig { isVisible: boolean; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx similarity index 73% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 54abc2c2bb667..80d33d1b95b61 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -18,14 +18,11 @@ import { } from '@elastic/charts'; import { xyChart, XYChart } from './xy_expression'; import { LensMultiTable } from '../types'; -import { - KibanaDatatable, - KibanaDatatableRow, -} from '../../../../../../src/plugins/expressions/public'; +import { KibanaDatatable, KibanaDatatableRow } from '../../../../../src/plugins/expressions/public'; import React from 'react'; import { shallow } from 'enzyme'; import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types'; -import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; const executeTriggerActions = jest.fn(); @@ -633,70 +630,242 @@ describe('xy_expression', () => { expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); }); - test('it names the series for multiple accessors', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; - - expect( - nameFn( - { - seriesKeys: ['a', 'b', 'c', 'd'], - key: '', - specId: 'a', - yAccessor: '', - splitAccessors: new Map(), + describe('provides correct series naming', () => { + const dataWithoutFormats: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a' }, + { id: 'b', name: 'b' }, + { id: 'c', name: 'c' }, + { id: 'd', name: 'd' }, + ], + rows: [ + { a: 1, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], }, - false - ) - ).toEqual('Label A - Label B - c - Label D'); - }); - - test('it names the series for a single accessor', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; - - expect( - nameFn( - { - seriesKeys: ['a', 'b', 'c', 'd'], - key: '', - specId: 'a', - yAccessor: '', - splitAccessors: new Map(), }, - false - ) - ).toEqual('Label A'); + }, + }; + + const nameFnArgs = { + seriesKeys: [], + key: '', + specId: 'a', + yAccessor: '', + splitAccessors: new Map(), + }; + + const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { + return shallow( + + ); + }; + + test('simplest xy chart without human-readable name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + // In this case, the ID is used as the name. This shouldn't happen in practice + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + }); + + test('simplest xy chart with empty name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '{"a":""}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + // In this case, the ID is used as the name. This shouldn't happen in practice + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + }); + + test('simplest xy chart with human-readable name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '{"a":"Column A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Column A'); + }); + + test('multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: undefined, + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + // This accessor has a human-readable name + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A'); + // This accessor does not + expect(nameFn({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + }); + + test('split series without formatting and single y accessor', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('split1'); + }); + + test('split series with formatting and single y accessor', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + convertSpy.mockReturnValueOnce('formatted'); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('formatted'); + expect(getFormatSpy).toHaveBeenCalledWith({ id: 'custom' }); + }); + + test('split series without formatting with multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A","b": "Label B"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + 'split1 - Label B' + ); + }); + + test('split series with formatting with multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A","b": "Label B"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2'); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( + 'formatted1 - Label A' + ); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + 'formatted2 - Label B' + ); + }); }); test('it set the scale of the x axis according to the args prop', () => { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx similarity index 88% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index f5798688badc5..f12a0e5b907c7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -28,14 +28,14 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EmbeddableVisTriggerContext } from '../../../../../../src/plugins/embeddable/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; +import { EmbeddableVisTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; import { LensMultiTable, FormatFactory } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; -import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; -import { parseInterval } from '../../../../../../src/plugins/data/common'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { parseInterval } from '../../../../../src/plugins/data/common'; import { getExecuteTriggerActions } from './services'; type InferPropType = T extends React.FunctionComponent ? P : T; @@ -361,12 +361,31 @@ export function XYChart({ enableHistogramMode: isHistogram && (seriesType.includes('stacked') || !splitAccessor), timeZone, name(d) { + const splitHint = table.columns.find(col => col.id === splitAccessor)?.formatHint; + + // For multiple y series, the name of the operation is used on each, either: + // * Key - Y name + // * Formatted value - Y name if (accessors.length > 1) { return d.seriesKeys - .map((key: string | number) => columnToLabelMap[key] || key) + .map((key: string | number, i) => { + if (i === 0 && splitHint) { + return formatFactory(splitHint).convert(key); + } + return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? ''; + }) .join(' - '); } - return columnToLabelMap[d.seriesKeys[0]] ?? d.seriesKeys[0]; + + // For formatted split series, format the key + // This handles splitting by dates, for example + if (splitHint) { + return formatFactory(splitHint).convert(d.seriesKeys[0]); + } + // This handles both split and single-y cases: + // * If split series without formatting, show the value literally + // * If single Y, the seriesKey will be the acccessor, so we show the human-readable name + return splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? ''; }, }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md similarity index 100% rename from x-pack/legacy/plugins/lens/readme.md rename to x-pack/plugins/lens/readme.md diff --git a/x-pack/plugins/lens/server/index.ts b/x-pack/plugins/lens/server/index.ts index 3b9e94986d247..8aeeeab4539b6 100644 --- a/x-pack/plugins/lens/server/index.ts +++ b/x-pack/plugins/lens/server/index.ts @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'kibana/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { LensServerPlugin } from './plugin'; export * from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + export const plugin = (initializerContext: PluginInitializerContext) => new LensServerPlugin(initializerContext); diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index e2462be149745..469457f973b62 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -184,7 +184,10 @@ export function telemetryTaskRunner( ) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = core.elasticsearch.adminClient.callAsInternalUser; + const callCluster = async (...args: Parameters) => { + const [coreStart] = await core.getStartServices(); + return coreStart.elasticsearch.legacy.client.callAsInternalUser(...args); + }; return { async run() { diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts index ceba2fe56db12..26044d28d53a3 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts @@ -65,7 +65,7 @@ export type DataMeta = Partial & export type DataRequestDescriptor = { dataId: string; - dataMetaAtStart?: DataMeta; + dataMetaAtStart?: DataMeta | null; dataRequestToken?: symbol; data?: object; dataMeta?: DataMeta; diff --git a/x-pack/plugins/maps/public/layers/util/data_request.ts b/x-pack/plugins/maps/public/layers/util/data_request.ts index eeef5c49c6ef8..44b7b2ffb6ae7 100644 --- a/x-pack/plugins/maps/public/layers/util/data_request.ts +++ b/x-pack/plugins/maps/public/layers/util/data_request.ts @@ -26,9 +26,13 @@ export class DataRequest { } getMeta(): DataMeta { - return this.hasData() - ? _.get(this._descriptor, 'dataMeta', {}) - : _.get(this._descriptor, 'dataMetaAtStart', {}); + if (this._descriptor.dataMetaAtStart) { + return this._descriptor.dataMetaAtStart; + } else if (this._descriptor.dataMeta) { + return this._descriptor.dataMeta; + } else { + return {}; + } } hasData(): boolean { diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 7e07569b44b83..251a2304538ed 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -57,8 +57,13 @@ const updateLayerInList = (state, layerId, attribute, newValue) => { if (!layerId) { return state; } + const { layerList } = state; const layerIdx = getLayerIndex(layerList, layerId); + if (layerIdx === -1) { + return state; + } + const updatedLayer = { ...layerList[layerIdx], // Update layer w/ new value. If no value provided, toggle boolean value diff --git a/x-pack/plugins/ml/common/constants/file_datavisualizer.ts b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts index 81d51bfa25816..7e18b36fd7bab 100644 --- a/x-pack/plugins/ml/common/constants/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export const MAX_BYTES = 104857600; -export const ABSOLUTE_MAX_BYTES = MAX_BYTES * 5; +export const MAX_FILE_SIZE = '100MB'; +export const MAX_FILE_SIZE_BYTES = 104857600; // 100MB + +export const ABSOLUTE_MAX_FILE_SIZE_BYTES = 1073741274; // 1GB export const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; // Value to use in the Elasticsearch index mapping meta data to identify the diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts index f771547b97811..c997a4e24f868 100644 --- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts @@ -67,13 +67,15 @@ export interface ImportResponse { export interface ImportFailure { item: number; reason: string; - doc: Doc; + doc: ImportDoc; } export interface Doc { message: string; } +export type ImportDoc = Doc | string; + export interface Settings { pipeline?: string; index: string; diff --git a/x-pack/plugins/ml/common/types/ml_config.ts b/x-pack/plugins/ml/common/types/ml_config.ts index 8fd9fd22bad8a..f2ddadccb2170 100644 --- a/x-pack/plugins/ml/common/types/ml_config.ts +++ b/x-pack/plugins/ml/common/types/ml_config.ts @@ -5,11 +5,11 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { MAX_BYTES } from '../constants/file_datavisualizer'; +import { MAX_FILE_SIZE } from '../constants/file_datavisualizer'; export const configSchema = schema.object({ file_data_visualizer: schema.object({ - max_file_size_bytes: schema.number({ defaultValue: MAX_BYTES }), + max_file_size: schema.string({ defaultValue: MAX_FILE_SIZE }), }), }); diff --git a/x-pack/plugins/ml/common/util/__tests__/anomaly_utils.js b/x-pack/plugins/ml/common/util/__tests__/anomaly_utils.js deleted file mode 100644 index 515304d222c8c..0000000000000 --- a/x-pack/plugins/ml/common/util/__tests__/anomaly_utils.js +++ /dev/null @@ -1,443 +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 expect from '@kbn/expect'; -import { - getSeverity, - getSeverityWithLow, - getSeverityColor, - getMultiBucketImpactLabel, - getEntityFieldName, - getEntityFieldValue, - getEntityFieldList, - showActualForFunction, - showTypicalForFunction, - isRuleSupported, - aggregationTypeTransform, -} from '../anomaly_utils'; - -describe('ML - anomaly utils', () => { - const partitionEntityRecord = { - job_id: 'farequote', - result_type: 'record', - probability: 0.012818, - record_score: 0.0162059, - bucket_span: 300, - detector_index: 0, - timestamp: 1455047400000, - partition_field_name: 'airline', - partition_field_value: 'AAL', - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - }; - - const byEntityRecord = { - job_id: 'farequote', - result_type: 'record', - probability: 0.012818, - record_score: 0.0162059, - bucket_span: 300, - detector_index: 0, - timestamp: 1455047400000, - by_field_name: 'airline', - by_field_value: 'JZA', - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - }; - - const overEntityRecord = { - job_id: 'gallery', - result_type: 'record', - probability: 2.81806e-9, - record_score: 59.055, - bucket_span: 3600, - detector_index: 4, - timestamp: 1420552800000, - function: 'sum', - function_description: 'sum', - field_name: 'bytes', - by_field_name: 'method', - over_field_name: 'clientip', - over_field_value: '37.157.32.164', - }; - - const noEntityRecord = { - job_id: 'farequote_no_by', - result_type: 'record', - probability: 0.0191711, - record_score: 4.38431, - initial_record_score: 19.654, - bucket_span: 300, - detector_index: 0, - timestamp: 1454890500000, - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - }; - - const metricNoEntityRecord = { - job_id: 'farequote_metric', - result_type: 'record', - probability: 0.030133495093182184, - record_score: 0.024881740359975164, - initial_record_score: 0.024881740359975164, - bucket_span: 900, - detector_index: 0, - is_interim: false, - timestamp: 1486845000000, - function: 'metric', - function_description: 'mean', - typical: [545.7764658569108], - actual: [758.8220213274412], - field_name: 'responsetime', - influencers: [ - { - influencer_field_name: 'airline', - influencer_field_values: ['NKS'], - }, - ], - airline: ['NKS'], - }; - - const rareEntityRecord = { - job_id: 'gallery', - result_type: 'record', - probability: 0.02277014211908481, - record_score: 4.545378107075983, - initial_record_score: 4.545378107075983, - bucket_span: 3600, - detector_index: 0, - is_interim: false, - timestamp: 1495879200000, - by_field_name: 'status', - function: 'rare', - function_description: 'rare', - over_field_name: 'clientip', - over_field_value: '173.252.74.112', - causes: [ - { - probability: 0.02277014211908481, - by_field_name: 'status', - by_field_value: '206', - function: 'rare', - function_description: 'rare', - typical: [0.00014832458182211878], - actual: [1], - over_field_name: 'clientip', - over_field_value: '173.252.74.112', - }, - ], - influencers: [ - { - influencer_field_name: 'uri', - influencer_field_values: [ - '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', - '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', - ], - }, - { - influencer_field_name: 'status', - influencer_field_values: ['206'], - }, - { - influencer_field_name: 'clientip', - influencer_field_values: ['173.252.74.112'], - }, - ], - clientip: ['173.252.74.112'], - uri: [ - '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', - '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', - ], - status: ['206'], - }; - - describe('getSeverity', () => { - it('returns warning for 0 <= score < 25', () => { - expect(getSeverity(0).id).to.be('warning'); - expect(getSeverity(0.001).id).to.be('warning'); - expect(getSeverity(24.99).id).to.be('warning'); - }); - - it('returns minor for 25 <= score < 50', () => { - expect(getSeverity(25).id).to.be('minor'); - expect(getSeverity(49.99).id).to.be('minor'); - }); - - it('returns minor for 50 <= score < 75', () => { - expect(getSeverity(50).id).to.be('major'); - expect(getSeverity(74.99).id).to.be('major'); - }); - - it('returns critical for score >= 75', () => { - expect(getSeverity(75).id).to.be('critical'); - expect(getSeverity(100).id).to.be('critical'); - expect(getSeverity(1000).id).to.be('critical'); - }); - - it('returns unknown for scores less than 0 or string input', () => { - expect(getSeverity(-10).id).to.be('unknown'); - expect(getSeverity('value').id).to.be('unknown'); - }); - }); - - describe('getSeverityWithLow', () => { - it('returns low for 0 <= score < 3', () => { - expect(getSeverityWithLow(0).id).to.be('low'); - expect(getSeverityWithLow(0.001).id).to.be('low'); - expect(getSeverityWithLow(2.99).id).to.be('low'); - }); - - it('returns warning for 3 <= score < 25', () => { - expect(getSeverityWithLow(3).id).to.be('warning'); - expect(getSeverityWithLow(24.99).id).to.be('warning'); - }); - - it('returns minor for 25 <= score < 50', () => { - expect(getSeverityWithLow(25).id).to.be('minor'); - expect(getSeverityWithLow(49.99).id).to.be('minor'); - }); - - it('returns minor for 50 <= score < 75', () => { - expect(getSeverityWithLow(50).id).to.be('major'); - expect(getSeverityWithLow(74.99).id).to.be('major'); - }); - - it('returns critical for score >= 75', () => { - expect(getSeverityWithLow(75).id).to.be('critical'); - expect(getSeverityWithLow(100).id).to.be('critical'); - expect(getSeverityWithLow(1000).id).to.be('critical'); - }); - - it('returns unknown for scores less than 0 or string input', () => { - expect(getSeverityWithLow(-10).id).to.be('unknown'); - expect(getSeverityWithLow('value').id).to.be('unknown'); - }); - }); - - describe('getSeverityColor', () => { - it('returns correct hex code for low for 0 <= score < 3', () => { - expect(getSeverityColor(0)).to.be('#d2e9f7'); - expect(getSeverityColor(0.001)).to.be('#d2e9f7'); - expect(getSeverityColor(2.99)).to.be('#d2e9f7'); - }); - - it('returns correct hex code for warning for 3 <= score < 25', () => { - expect(getSeverityColor(3)).to.be('#8bc8fb'); - expect(getSeverityColor(24.99)).to.be('#8bc8fb'); - }); - - it('returns correct hex code for minor for 25 <= score < 50', () => { - expect(getSeverityColor(25)).to.be('#fdec25'); - expect(getSeverityColor(49.99)).to.be('#fdec25'); - }); - - it('returns correct hex code for major for 50 <= score < 75', () => { - expect(getSeverityColor(50)).to.be('#fba740'); - expect(getSeverityColor(74.99)).to.be('#fba740'); - }); - - it('returns correct hex code for critical for score >= 75', () => { - expect(getSeverityColor(75)).to.be('#fe5050'); - expect(getSeverityColor(100)).to.be('#fe5050'); - expect(getSeverityColor(1000)).to.be('#fe5050'); - }); - - it('returns correct hex code for unknown for scores less than 0 or string input', () => { - expect(getSeverityColor(-10)).to.be('#ffffff'); - expect(getSeverityColor('value')).to.be('#ffffff'); - }); - }); - - describe('getMultiBucketImpactLabel', () => { - it('returns high for 3 <= score <= 5', () => { - expect(getMultiBucketImpactLabel(3)).to.be('high'); - expect(getMultiBucketImpactLabel(5)).to.be('high'); - }); - - it('returns medium for 2 <= score < 3', () => { - expect(getMultiBucketImpactLabel(2)).to.be('medium'); - expect(getMultiBucketImpactLabel(2.99)).to.be('medium'); - }); - - it('returns low for 1 <= score < 2', () => { - expect(getMultiBucketImpactLabel(1)).to.be('low'); - expect(getMultiBucketImpactLabel(1.99)).to.be('low'); - }); - - it('returns none for -5 <= score < 1', () => { - expect(getMultiBucketImpactLabel(-5)).to.be('none'); - expect(getMultiBucketImpactLabel(0.99)).to.be('none'); - }); - - it('returns expected label when impact outside normal bounds', () => { - expect(getMultiBucketImpactLabel(10)).to.be('high'); - expect(getMultiBucketImpactLabel(-10)).to.be('none'); - }); - }); - - describe('getEntityFieldName', () => { - it('returns the by field name', () => { - expect(getEntityFieldName(byEntityRecord)).to.be('airline'); - }); - - it('returns the partition field name', () => { - expect(getEntityFieldName(partitionEntityRecord)).to.be('airline'); - }); - - it('returns the over field name', () => { - expect(getEntityFieldName(overEntityRecord)).to.be('clientip'); - }); - - it('returns undefined if no by, over or partition fields', () => { - expect(getEntityFieldName(noEntityRecord)).to.be(undefined); - }); - }); - - describe('getEntityFieldValue', () => { - it('returns the by field value', () => { - expect(getEntityFieldValue(byEntityRecord)).to.be('JZA'); - }); - - it('returns the partition field value', () => { - expect(getEntityFieldValue(partitionEntityRecord)).to.be('AAL'); - }); - - it('returns the over field value', () => { - expect(getEntityFieldValue(overEntityRecord)).to.be('37.157.32.164'); - }); - - it('returns undefined if no by, over or partition fields', () => { - expect(getEntityFieldValue(noEntityRecord)).to.be(undefined); - }); - }); - - describe('getEntityFieldList', () => { - it('returns an empty list for a record with no by, over or partition fields', () => { - expect(getEntityFieldList(noEntityRecord)).to.be.empty(); - }); - - it('returns correct list for a record with a by field', () => { - expect(getEntityFieldList(byEntityRecord)).to.eql([ - { - fieldName: 'airline', - fieldValue: 'JZA', - fieldType: 'by', - }, - ]); - }); - - it('returns correct list for a record with a partition field', () => { - expect(getEntityFieldList(partitionEntityRecord)).to.eql([ - { - fieldName: 'airline', - fieldValue: 'AAL', - fieldType: 'partition', - }, - ]); - }); - - it('returns correct list for a record with an over field', () => { - expect(getEntityFieldList(overEntityRecord)).to.eql([ - { - fieldName: 'clientip', - fieldValue: '37.157.32.164', - fieldType: 'over', - }, - ]); - }); - - it('returns correct list for a record with a by and over field', () => { - expect(getEntityFieldList(rareEntityRecord)).to.eql([ - { - fieldName: 'clientip', - fieldValue: '173.252.74.112', - fieldType: 'over', - }, - ]); - }); - }); - - describe('showActualForFunction', () => { - it('returns true for expected function descriptions', () => { - expect(showActualForFunction('count')).to.be(true); - expect(showActualForFunction('distinct_count')).to.be(true); - expect(showActualForFunction('lat_long')).to.be(true); - expect(showActualForFunction('mean')).to.be(true); - expect(showActualForFunction('max')).to.be(true); - expect(showActualForFunction('min')).to.be(true); - expect(showActualForFunction('sum')).to.be(true); - expect(showActualForFunction('median')).to.be(true); - expect(showActualForFunction('varp')).to.be(true); - expect(showActualForFunction('info_content')).to.be(true); - expect(showActualForFunction('time')).to.be(true); - }); - - it('returns false for expected function descriptions', () => { - expect(showActualForFunction('rare')).to.be(false); - }); - }); - - describe('showTypicalForFunction', () => { - it('returns true for expected function descriptions', () => { - expect(showTypicalForFunction('count')).to.be(true); - expect(showTypicalForFunction('distinct_count')).to.be(true); - expect(showTypicalForFunction('lat_long')).to.be(true); - expect(showTypicalForFunction('mean')).to.be(true); - expect(showTypicalForFunction('max')).to.be(true); - expect(showTypicalForFunction('min')).to.be(true); - expect(showTypicalForFunction('sum')).to.be(true); - expect(showTypicalForFunction('median')).to.be(true); - expect(showTypicalForFunction('varp')).to.be(true); - expect(showTypicalForFunction('info_content')).to.be(true); - expect(showTypicalForFunction('time')).to.be(true); - }); - - it('returns false for expected function descriptions', () => { - expect(showTypicalForFunction('rare')).to.be(false); - }); - }); - - describe('isRuleSupported', () => { - it('returns true for anomalies supporting rules', () => { - expect(isRuleSupported(partitionEntityRecord)).to.be(true); - expect(isRuleSupported(byEntityRecord)).to.be(true); - expect(isRuleSupported(overEntityRecord)).to.be(true); - expect(isRuleSupported(rareEntityRecord)).to.be(true); - expect(isRuleSupported(noEntityRecord)).to.be(true); - }); - - it('returns false for anomaly not supporting rules', () => { - expect(isRuleSupported(metricNoEntityRecord)).to.be(false); - }); - }); - - describe('aggregationTypeTransform', () => { - it('returns correct ES aggregation type for ML function description', () => { - expect(aggregationTypeTransform.toES('count')).to.be('count'); - expect(aggregationTypeTransform.toES('distinct_count')).to.be('cardinality'); - expect(aggregationTypeTransform.toES('distinct_count')).to.not.be('distinct_count'); - expect(aggregationTypeTransform.toES('mean')).to.be('avg'); - expect(aggregationTypeTransform.toES('mean')).to.not.be('mean'); - expect(aggregationTypeTransform.toES('max')).to.be('max'); - expect(aggregationTypeTransform.toES('min')).to.be('min'); - expect(aggregationTypeTransform.toES('sum')).to.be('sum'); - }); - - it('returns correct ML function description for ES aggregation type', () => { - expect(aggregationTypeTransform.toML('count')).to.be('count'); - expect(aggregationTypeTransform.toML('cardinality')).to.be('distinct_count'); - expect(aggregationTypeTransform.toML('cardinality')).to.not.be('cardinality'); - expect(aggregationTypeTransform.toML('avg')).to.be('mean'); - expect(aggregationTypeTransform.toML('avg')).to.not.be('avg'); - expect(aggregationTypeTransform.toML('max')).to.be('max'); - expect(aggregationTypeTransform.toML('min')).to.be('min'); - expect(aggregationTypeTransform.toML('sum')).to.be('sum'); - }); - }); -}); diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.d.ts b/x-pack/plugins/ml/common/util/anomaly_utils.d.ts deleted file mode 100644 index adeb6dc7dd5b9..0000000000000 --- a/x-pack/plugins/ml/common/util/anomaly_utils.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ANOMALY_SEVERITY } from '../constants/anomalies'; - -export function getSeverity(normalizedScore: number): string; -export function getSeverityType(normalizedScore: number): ANOMALY_SEVERITY; -export function getSeverityColor(normalizedScore: number): string; diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.test.ts b/x-pack/plugins/ml/common/util/anomaly_utils.test.ts new file mode 100644 index 0000000000000..1343e4611c215 --- /dev/null +++ b/x-pack/plugins/ml/common/util/anomaly_utils.test.ts @@ -0,0 +1,444 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { AnomalyRecordDoc } from '../types/anomalies'; + +import { + aggregationTypeTransform, + getEntityFieldList, + getEntityFieldName, + getEntityFieldValue, + getMultiBucketImpactLabel, + getSeverity, + getSeverityWithLow, + getSeverityColor, + isRuleSupported, + showActualForFunction, + showTypicalForFunction, +} from './anomaly_utils'; + +describe('ML - anomaly utils', () => { + const partitionEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote', + result_type: 'record', + probability: 0.012818, + record_score: 0.0162059, + initial_record_score: 0.0162059, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1455047400000, + partition_field_name: 'airline', + partition_field_value: 'AAL', + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + }; + + const byEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote', + result_type: 'record', + probability: 0.012818, + record_score: 0.0162059, + initial_record_score: 0.0162059, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1455047400000, + by_field_name: 'airline', + by_field_value: 'JZA', + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + }; + + const overEntityRecord: AnomalyRecordDoc = { + job_id: 'gallery', + result_type: 'record', + probability: 2.81806e-9, + record_score: 59.055, + initial_record_score: 59.055, + bucket_span: 3600, + detector_index: 4, + is_interim: false, + timestamp: 1420552800000, + function: 'sum', + function_description: 'sum', + field_name: 'bytes', + by_field_name: 'method', + over_field_name: 'clientip', + over_field_value: '37.157.32.164', + }; + + const noEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote_no_by', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + }; + + const metricNoEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote_metric', + result_type: 'record', + probability: 0.030133495093182184, + record_score: 0.024881740359975164, + initial_record_score: 0.024881740359975164, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1486845000000, + function: 'metric', + function_description: 'mean', + typical: [545.7764658569108], + actual: [758.8220213274412], + field_name: 'responsetime', + influencers: [ + { + influencer_field_name: 'airline', + influencer_field_values: ['NKS'], + }, + ], + airline: ['NKS'], + }; + + const rareEntityRecord: AnomalyRecordDoc = { + job_id: 'gallery', + result_type: 'record', + probability: 0.02277014211908481, + record_score: 4.545378107075983, + initial_record_score: 4.545378107075983, + bucket_span: 3600, + detector_index: 0, + is_interim: false, + timestamp: 1495879200000, + by_field_name: 'status', + function: 'rare', + function_description: 'rare', + over_field_name: 'clientip', + over_field_value: '173.252.74.112', + causes: [ + { + probability: 0.02277014211908481, + by_field_name: 'status', + by_field_value: '206', + function: 'rare', + function_description: 'rare', + typical: [0.00014832458182211878], + actual: [1], + over_field_name: 'clientip', + over_field_value: '173.252.74.112', + }, + ], + influencers: [ + { + influencer_field_name: 'uri', + influencer_field_values: [ + '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', + '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', + ], + }, + { + influencer_field_name: 'status', + influencer_field_values: ['206'], + }, + { + influencer_field_name: 'clientip', + influencer_field_values: ['173.252.74.112'], + }, + ], + clientip: ['173.252.74.112'], + uri: [ + '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', + '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', + ], + status: ['206'], + }; + + describe('getSeverity', () => { + test('returns warning for 0 <= score < 25', () => { + expect(getSeverity(0).id).toBe('warning'); + expect(getSeverity(0.001).id).toBe('warning'); + expect(getSeverity(24.99).id).toBe('warning'); + }); + + test('returns minor for 25 <= score < 50', () => { + expect(getSeverity(25).id).toBe('minor'); + expect(getSeverity(49.99).id).toBe('minor'); + }); + + test('returns minor for 50 <= score < 75', () => { + expect(getSeverity(50).id).toBe('major'); + expect(getSeverity(74.99).id).toBe('major'); + }); + + test('returns critical for score >= 75', () => { + expect(getSeverity(75).id).toBe('critical'); + expect(getSeverity(100).id).toBe('critical'); + expect(getSeverity(1000).id).toBe('critical'); + }); + + test('returns unknown for scores less than 0', () => { + expect(getSeverity(-10).id).toBe('unknown'); + }); + }); + + describe('getSeverityWithLow', () => { + test('returns low for 0 <= score < 3', () => { + expect(getSeverityWithLow(0).id).toBe('low'); + expect(getSeverityWithLow(0.001).id).toBe('low'); + expect(getSeverityWithLow(2.99).id).toBe('low'); + }); + + test('returns warning for 3 <= score < 25', () => { + expect(getSeverityWithLow(3).id).toBe('warning'); + expect(getSeverityWithLow(24.99).id).toBe('warning'); + }); + + test('returns minor for 25 <= score < 50', () => { + expect(getSeverityWithLow(25).id).toBe('minor'); + expect(getSeverityWithLow(49.99).id).toBe('minor'); + }); + + test('returns minor for 50 <= score < 75', () => { + expect(getSeverityWithLow(50).id).toBe('major'); + expect(getSeverityWithLow(74.99).id).toBe('major'); + }); + + test('returns critical for score >= 75', () => { + expect(getSeverityWithLow(75).id).toBe('critical'); + expect(getSeverityWithLow(100).id).toBe('critical'); + expect(getSeverityWithLow(1000).id).toBe('critical'); + }); + + test('returns unknown for scores less than 0 ', () => { + expect(getSeverityWithLow(-10).id).toBe('unknown'); + }); + }); + + describe('getSeverityColor', () => { + test('returns correct hex code for low for 0 <= score < 3', () => { + expect(getSeverityColor(0)).toBe('#d2e9f7'); + expect(getSeverityColor(0.001)).toBe('#d2e9f7'); + expect(getSeverityColor(2.99)).toBe('#d2e9f7'); + }); + + test('returns correct hex code for warning for 3 <= score < 25', () => { + expect(getSeverityColor(3)).toBe('#8bc8fb'); + expect(getSeverityColor(24.99)).toBe('#8bc8fb'); + }); + + test('returns correct hex code for minor for 25 <= score < 50', () => { + expect(getSeverityColor(25)).toBe('#fdec25'); + expect(getSeverityColor(49.99)).toBe('#fdec25'); + }); + + test('returns correct hex code for major for 50 <= score < 75', () => { + expect(getSeverityColor(50)).toBe('#fba740'); + expect(getSeverityColor(74.99)).toBe('#fba740'); + }); + + test('returns correct hex code for critical for score >= 75', () => { + expect(getSeverityColor(75)).toBe('#fe5050'); + expect(getSeverityColor(100)).toBe('#fe5050'); + expect(getSeverityColor(1000)).toBe('#fe5050'); + }); + + test('returns correct hex code for unknown for scores less than 0', () => { + expect(getSeverityColor(-10)).toBe('#ffffff'); + }); + }); + + describe('getMultiBucketImpactLabel', () => { + test('returns high for 3 <= score <= 5', () => { + expect(getMultiBucketImpactLabel(3)).toBe('high'); + expect(getMultiBucketImpactLabel(5)).toBe('high'); + }); + + test('returns medium for 2 <= score < 3', () => { + expect(getMultiBucketImpactLabel(2)).toBe('medium'); + expect(getMultiBucketImpactLabel(2.99)).toBe('medium'); + }); + + test('returns low for 1 <= score < 2', () => { + expect(getMultiBucketImpactLabel(1)).toBe('low'); + expect(getMultiBucketImpactLabel(1.99)).toBe('low'); + }); + + test('returns none for -5 <= score < 1', () => { + expect(getMultiBucketImpactLabel(-5)).toBe('none'); + expect(getMultiBucketImpactLabel(0.99)).toBe('none'); + }); + + test('returns expected label when impact outside normal bounds', () => { + expect(getMultiBucketImpactLabel(10)).toBe('high'); + expect(getMultiBucketImpactLabel(-10)).toBe('none'); + }); + }); + + describe('getEntityFieldName', () => { + it('returns the by field name', () => { + expect(getEntityFieldName(byEntityRecord)).toBe('airline'); + }); + + it('returns the partition field name', () => { + expect(getEntityFieldName(partitionEntityRecord)).toBe('airline'); + }); + + it('returns the over field name', () => { + expect(getEntityFieldName(overEntityRecord)).toBe('clientip'); + }); + + it('returns undefined if no by, over or partition fields', () => { + expect(getEntityFieldName(noEntityRecord)).toBe(undefined); + }); + }); + + describe('getEntityFieldValue', () => { + test('returns the by field value', () => { + expect(getEntityFieldValue(byEntityRecord)).toBe('JZA'); + }); + + test('returns the partition field value', () => { + expect(getEntityFieldValue(partitionEntityRecord)).toBe('AAL'); + }); + + test('returns the over field value', () => { + expect(getEntityFieldValue(overEntityRecord)).toBe('37.157.32.164'); + }); + + test('returns undefined if no by, over or partition fields', () => { + expect(getEntityFieldValue(noEntityRecord)).toBe(undefined); + }); + }); + + describe('getEntityFieldList', () => { + test('returns an empty list for a record with no by, over or partition fields', () => { + expect(getEntityFieldList(noEntityRecord)).toHaveLength(0); + }); + + test('returns correct list for a record with a by field', () => { + expect(getEntityFieldList(byEntityRecord)).toEqual([ + { + fieldName: 'airline', + fieldValue: 'JZA', + fieldType: 'by', + }, + ]); + }); + + test('returns correct list for a record with a partition field', () => { + expect(getEntityFieldList(partitionEntityRecord)).toEqual([ + { + fieldName: 'airline', + fieldValue: 'AAL', + fieldType: 'partition', + }, + ]); + }); + + test('returns correct list for a record with an over field', () => { + expect(getEntityFieldList(overEntityRecord)).toEqual([ + { + fieldName: 'clientip', + fieldValue: '37.157.32.164', + fieldType: 'over', + }, + ]); + }); + + test('returns correct list for a record with a by and over field', () => { + expect(getEntityFieldList(rareEntityRecord)).toEqual([ + { + fieldName: 'clientip', + fieldValue: '173.252.74.112', + fieldType: 'over', + }, + ]); + }); + }); + + describe('showActualForFunction', () => { + test('returns true for expected function descriptions', () => { + expect(showActualForFunction('count')).toBe(true); + expect(showActualForFunction('distinct_count')).toBe(true); + expect(showActualForFunction('lat_long')).toBe(true); + expect(showActualForFunction('mean')).toBe(true); + expect(showActualForFunction('max')).toBe(true); + expect(showActualForFunction('min')).toBe(true); + expect(showActualForFunction('sum')).toBe(true); + expect(showActualForFunction('median')).toBe(true); + expect(showActualForFunction('varp')).toBe(true); + expect(showActualForFunction('info_content')).toBe(true); + expect(showActualForFunction('time')).toBe(true); + }); + + test('returns false for expected function descriptions', () => { + expect(showActualForFunction('rare')).toBe(false); + }); + }); + + describe('showTypicalForFunction', () => { + test('returns true for expected function descriptions', () => { + expect(showTypicalForFunction('count')).toBe(true); + expect(showTypicalForFunction('distinct_count')).toBe(true); + expect(showTypicalForFunction('lat_long')).toBe(true); + expect(showTypicalForFunction('mean')).toBe(true); + expect(showTypicalForFunction('max')).toBe(true); + expect(showTypicalForFunction('min')).toBe(true); + expect(showTypicalForFunction('sum')).toBe(true); + expect(showTypicalForFunction('median')).toBe(true); + expect(showTypicalForFunction('varp')).toBe(true); + expect(showTypicalForFunction('info_content')).toBe(true); + expect(showTypicalForFunction('time')).toBe(true); + }); + + test('returns false for expected function descriptions', () => { + expect(showTypicalForFunction('rare')).toBe(false); + }); + }); + + describe('isRuleSupported', () => { + test('returns true for anomalies supporting rules', () => { + expect(isRuleSupported(partitionEntityRecord)).toBe(true); + expect(isRuleSupported(byEntityRecord)).toBe(true); + expect(isRuleSupported(overEntityRecord)).toBe(true); + expect(isRuleSupported(rareEntityRecord)).toBe(true); + expect(isRuleSupported(noEntityRecord)).toBe(true); + }); + + it('returns false for anomaly not supporting rules', () => { + expect(isRuleSupported(metricNoEntityRecord)).toBe(false); + }); + }); + + describe('aggregationTypeTransform', () => { + test('returns correct ES aggregation type for ML function description', () => { + expect(aggregationTypeTransform.toES('count')).toBe('count'); + expect(aggregationTypeTransform.toES('distinct_count')).toBe('cardinality'); + expect(aggregationTypeTransform.toES('mean')).toBe('avg'); + expect(aggregationTypeTransform.toES('max')).toBe('max'); + expect(aggregationTypeTransform.toES('min')).toBe('min'); + expect(aggregationTypeTransform.toES('sum')).toBe('sum'); + }); + + test('returns correct ML function description for ES aggregation type', () => { + expect(aggregationTypeTransform.toML('count')).toBe('count'); + expect(aggregationTypeTransform.toML('cardinality')).toBe('distinct_count'); + expect(aggregationTypeTransform.toML('avg')).toBe('mean'); + expect(aggregationTypeTransform.toML('max')).toBe('max'); + expect(aggregationTypeTransform.toML('min')).toBe('min'); + expect(aggregationTypeTransform.toML('sum')).toBe('sum'); + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.js b/x-pack/plugins/ml/common/util/anomaly_utils.ts similarity index 87% rename from x-pack/plugins/ml/common/util/anomaly_utils.js rename to x-pack/plugins/ml/common/util/anomaly_utils.ts index 16c27b6af869d..36b91f5580b39 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.js +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -13,6 +13,24 @@ import { i18n } from '@kbn/i18n'; import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../constants/anomalies'; +import { AnomalyRecordDoc } from '../types/anomalies'; + +export interface SeverityType { + id: ANOMALY_SEVERITY; + label: string; +} + +export enum ENTITY_FIELD_TYPE { + BY = 'by', + OVER = 'over', + PARTITON = 'partition', +} + +export interface EntityField { + fieldName: string; + fieldValue: string | number | undefined; + fieldType: ENTITY_FIELD_TYPE; +} // List of function descriptions for which actual values from record level results should be displayed. const DISPLAY_ACTUAL_FUNCTIONS = [ @@ -44,7 +62,7 @@ const DISPLAY_TYPICAL_FUNCTIONS = [ 'time', ]; -let severityTypes; +let severityTypes: Record; function getSeverityTypes() { if (severityTypes) { @@ -93,7 +111,7 @@ function getSeverityTypes() { // Returns a severity label (one of critical, major, minor, warning or unknown) // for the supplied normalized anomaly score (a value between 0 and 100). -export function getSeverity(normalizedScore) { +export function getSeverity(normalizedScore: number): SeverityType { const severityTypesList = getSeverityTypes(); if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { @@ -109,7 +127,7 @@ export function getSeverity(normalizedScore) { } } -export function getSeverityType(normalizedScore) { +export function getSeverityType(normalizedScore: number): ANOMALY_SEVERITY { if (normalizedScore >= 75) { return ANOMALY_SEVERITY.CRITICAL; } else if (normalizedScore >= 50) { @@ -128,7 +146,7 @@ export function getSeverityType(normalizedScore) { // Returns a severity label (one of critical, major, minor, warning, low or unknown) // for the supplied normalized anomaly score (a value between 0 and 100), where scores // less than 3 are assigned a severity of 'low'. -export function getSeverityWithLow(normalizedScore) { +export function getSeverityWithLow(normalizedScore: number): SeverityType { const severityTypesList = getSeverityTypes(); if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { @@ -148,7 +166,7 @@ export function getSeverityWithLow(normalizedScore) { // Returns a severity RGB color (one of critical, major, minor, warning, low_warning or unknown) // for the supplied normalized anomaly score (a value between 0 and 100). -export function getSeverityColor(normalizedScore) { +export function getSeverityColor(normalizedScore: number): string { if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { return '#fe5050'; } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { @@ -167,7 +185,7 @@ export function getSeverityColor(normalizedScore) { // Returns a label to use for the multi-bucket impact of an anomaly // according to the value of the multi_bucket_impact field of a record, // which ranges from -5 to +5. -export function getMultiBucketImpactLabel(multiBucketImpact) { +export function getMultiBucketImpactLabel(multiBucketImpact: number): string { if (multiBucketImpact >= MULTI_BUCKET_IMPACT.HIGH) { return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.highLabel', { defaultMessage: 'high', @@ -190,7 +208,7 @@ export function getMultiBucketImpactLabel(multiBucketImpact) { // Returns the name of the field to use as the entity name from the source record // obtained from Elasticsearch. The function looks first for a by_field, then over_field, // then partition_field, returning undefined if none of these fields are present. -export function getEntityFieldName(record) { +export function getEntityFieldName(record: AnomalyRecordDoc): string | undefined { // Analyses with by and over fields, will have a top-level by_field_name, but // the by_field_value(s) will be in the nested causes array. if (record.by_field_name !== undefined && record.by_field_value !== undefined) { @@ -211,7 +229,7 @@ export function getEntityFieldName(record) { // Returns the value of the field to use as the entity value from the source record // obtained from Elasticsearch. The function looks first for a by_field, then over_field, // then partition_field, returning undefined if none of these fields are present. -export function getEntityFieldValue(record) { +export function getEntityFieldValue(record: AnomalyRecordDoc): string | number | undefined { if (record.by_field_value !== undefined) { return record.by_field_value; } @@ -229,13 +247,13 @@ export function getEntityFieldValue(record) { // Returns the list of partitioning entity fields for the source record as a list // of objects in the form { fieldName: airline, fieldValue: AAL, fieldType: partition } -export function getEntityFieldList(record) { - const entityFields = []; +export function getEntityFieldList(record: AnomalyRecordDoc): EntityField[] { + const entityFields: EntityField[] = []; if (record.partition_field_name !== undefined) { entityFields.push({ fieldName: record.partition_field_name, fieldValue: record.partition_field_value, - fieldType: 'partition', + fieldType: ENTITY_FIELD_TYPE.PARTITON, }); } @@ -243,7 +261,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.over_field_name, fieldValue: record.over_field_value, - fieldType: 'over', + fieldType: ENTITY_FIELD_TYPE.OVER, }); } @@ -254,7 +272,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.by_field_name, fieldValue: record.by_field_value, - fieldType: 'by', + fieldType: ENTITY_FIELD_TYPE.BY, }); } @@ -264,19 +282,19 @@ export function getEntityFieldList(record) { // Returns whether actual values should be displayed for a record with the specified function description. // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. -export function showActualForFunction(functionDescription) { +export function showActualForFunction(functionDescription: string): boolean { return DISPLAY_ACTUAL_FUNCTIONS.indexOf(functionDescription) > -1; } // Returns whether typical values should be displayed for a record with the specified function description. // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. -export function showTypicalForFunction(functionDescription) { +export function showTypicalForFunction(functionDescription: string): boolean { return DISPLAY_TYPICAL_FUNCTIONS.indexOf(functionDescription) > -1; } // Returns whether a rule can be configured against the specified anomaly. -export function isRuleSupported(record) { +export function isRuleSupported(record: AnomalyRecordDoc): boolean { // A rule can be configured with a numeric condition if the function supports it, // and/or with scope if there is a partitioning fields. return ( @@ -303,7 +321,7 @@ export function isRuleSupported(record) { // The input to toES and the output from toML correspond to the value of the // function_description field of anomaly records. export const aggregationTypeTransform = { - toES: function(oldAggType) { + toES(oldAggType: string): string { let newAggType = oldAggType; if (newAggType === 'mean') { @@ -316,7 +334,7 @@ export const aggregationTypeTransform = { return newAggType; }, - toML: function(oldAggType) { + toML(oldAggType: string): string { let newAggType = oldAggType; if (newAggType === 'avg') { diff --git a/x-pack/plugins/ml/package.json b/x-pack/plugins/ml/package.json new file mode 100644 index 0000000000000..739dd806fcbb9 --- /dev/null +++ b/x-pack/plugins/ml/package.json @@ -0,0 +1,15 @@ +{ + "author": "Elastic", + "name": "ml", + "version": "0.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "build:apiDocScripts": "cd server/routes/apidoc_scripts && tsc", + "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md" + }, + "devDependencies": { + "apidoc": "^0.20.1", + "apidoc-markdown": "^5.0.0" + } +} diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/__tests__/utils.js b/x-pack/plugins/ml/public/application/components/rule_editor/utils.test.js similarity index 66% rename from x-pack/plugins/ml/public/application/components/rule_editor/__tests__/utils.js rename to x-pack/plugins/ml/public/application/components/rule_editor/utils.test.js index b5f9bdeaa12aa..18e382f8fe5e8 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/__tests__/utils.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/utils.test.js @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { isValidRule, buildRuleDescription, getAppliesToValueFromAnomaly } from '../utils'; +import { isValidRule, buildRuleDescription, getAppliesToValueFromAnomaly } from './utils'; import { ACTION, APPLIES_TO, OPERATOR, FILTER_TYPE, -} from '../../../../../common/constants/detector_rule'; +} from '../../../../common/constants/detector_rule'; describe('ML - rule editor utils', () => { const ruleWithCondition = { @@ -55,19 +54,19 @@ describe('ML - rule editor utils', () => { }; describe('isValidRule', () => { - it('returns true for a rule with an action and a condition', () => { - expect(isValidRule(ruleWithCondition)).to.be(true); + test('returns true for a rule with an action and a condition', () => { + expect(isValidRule(ruleWithCondition)).toBe(true); }); - it('returns true for a rule with an action and scope', () => { - expect(isValidRule(ruleWithScope)).to.be(true); + test('returns true for a rule with an action and scope', () => { + expect(isValidRule(ruleWithScope)).toBe(true); }); - it('returns true for a rule with an action, scope and condition', () => { - expect(isValidRule(ruleWithConditionAndScope)).to.be(true); + test('returns true for a rule with an action, scope and condition', () => { + expect(isValidRule(ruleWithConditionAndScope)).toBe(true); }); - it('returns false for a rule with no action', () => { + test('returns false for a rule with no action', () => { const ruleWithNoAction = { actions: [], conditions: [ @@ -79,27 +78,27 @@ describe('ML - rule editor utils', () => { ], }; - expect(isValidRule(ruleWithNoAction)).to.be(false); + expect(isValidRule(ruleWithNoAction)).toBe(false); }); - it('returns false for a rule with no scope or conditions', () => { + test('returns false for a rule with no scope or conditions', () => { const ruleWithNoScopeOrCondition = { actions: [ACTION.SKIP_RESULT], }; - expect(isValidRule(ruleWithNoScopeOrCondition)).to.be(false); + expect(isValidRule(ruleWithNoScopeOrCondition)).toBe(false); }); }); describe('buildRuleDescription', () => { - it('returns expected rule descriptions', () => { - expect(buildRuleDescription(ruleWithCondition)).to.be( + test('returns expected rule descriptions', () => { + expect(buildRuleDescription(ruleWithCondition)).toBe( 'skip result when actual is greater than 10' ); - expect(buildRuleDescription(ruleWithScope)).to.be( + expect(buildRuleDescription(ruleWithScope)).toBe( 'skip result when instance is in test_aws_instances' ); - expect(buildRuleDescription(ruleWithConditionAndScope)).to.be( + expect(buildRuleDescription(ruleWithConditionAndScope)).toBe( 'skip result when typical is less than 100 AND instance is not in test_aws_instances' ); }); @@ -111,16 +110,16 @@ describe('ML - rule editor utils', () => { typical: [1.23], }; - it('returns expected actual value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.ACTUAL)).to.be(210); + test('returns expected actual value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.ACTUAL)).toBe(210); }); - it('returns expected typical value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.TYPICAL)).to.be(1.23); + test('returns expected typical value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.TYPICAL)).toBe(1.23); }); - it('returns expected diff from typical value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.DIFF_FROM_TYPICAL)).to.be(208.77); + test('returns expected diff from typical value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.DIFF_FROM_TYPICAL)).toBe(208.77); }); }); }); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index d1b615a878b2b..c73ab4b9e11c7 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -24,14 +24,11 @@ import { readFile, createUrlOverrides, processResults, - reduceData, hasImportPermission, } from '../utils'; import { MODE } from './constants'; -const UPLOAD_SIZE_MB = 5; - export class FileDataVisualizerView extends Component { constructor(props) { super(props); @@ -40,6 +37,7 @@ export class FileDataVisualizerView extends Component { files: {}, fileName: '', fileContents: '', + data: [], fileSize: 0, fileTooLarge: false, fileCouldNotBeRead: false, @@ -79,6 +77,7 @@ export class FileDataVisualizerView extends Component { loaded: false, fileName: '', fileContents: '', + data: [], fileSize: 0, fileTooLarge: false, fileCouldNotBeRead: false, @@ -97,15 +96,15 @@ export class FileDataVisualizerView extends Component { async loadFile(file) { if (file.size <= this.maxFileUploadBytes) { try { - const fileContents = await readFile(file); - const data = fileContents.data; + const { data, fileContents } = await readFile(file); this.setState({ - fileContents: data, + data, + fileContents, fileName: file.name, fileSize: file.size, }); - await this.loadSettings(data); + await this.analyzeFile(fileContents); } catch (error) { this.setState({ loaded: false, @@ -124,14 +123,9 @@ export class FileDataVisualizerView extends Component { } } - async loadSettings(data, overrides, isRetry = false) { + async analyzeFile(fileContents, overrides, isRetry = false) { try { - // reduce the amount of data being sent to the endpoint - // 5MB should be enough to contain 1000 lines - const lessData = reduceData(data, UPLOAD_SIZE_MB); - console.log('overrides', overrides); - const { analyzeFile } = ml.fileDatavisualizer; - const resp = await analyzeFile(lessData, overrides); + const resp = await ml.fileDatavisualizer.analyzeFile(fileContents, overrides); const serverSettings = processResults(resp); const serverOverrides = resp.overrides; @@ -198,7 +192,7 @@ export class FileDataVisualizerView extends Component { loading: true, loaded: false, }); - this.loadSettings(data, this.previousOverrides, true); + this.analyzeFile(fileContents, this.previousOverrides, true); } } } @@ -240,7 +234,7 @@ export class FileDataVisualizerView extends Component { }, () => { const formattedOverrides = createUrlOverrides(overrides, this.originalSettings); - this.loadSettings(this.state.fileContents, formattedOverrides); + this.analyzeFile(this.state.fileContents, formattedOverrides); } ); }; @@ -261,6 +255,7 @@ export class FileDataVisualizerView extends Component { results, explanation, fileContents, + data, fileName, fileSize, fileTooLarge, @@ -339,6 +334,7 @@ export class FileDataVisualizerView extends Component { results={results} fileName={fileName} fileContents={fileContents} + data={data} indexPatterns={this.props.indexPatterns} kibanaConfig={this.props.kibanaConfig} showBottomBar={this.showBottomBar} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index 4c9579bfd4b46..2bf7bbeb641d0 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -94,7 +94,7 @@ export class ImportView extends Component { // TODO - sort this function out. it's a mess async import() { - const { fileContents, results, indexPatterns, kibanaConfig, showBottomBar } = this.props; + const { data, results, indexPatterns, kibanaConfig, showBottomBar } = this.props; const { format } = results; let { timeFieldName } = this.state; @@ -217,7 +217,7 @@ export class ImportView extends Component { if (success) { const importer = importerFactory(format, results, indexCreationSettings); if (importer !== undefined) { - const readResp = importer.read(fileContents, this.setReadProgress); + const readResp = importer.read(data, this.setReadProgress); success = readResp.success; this.setState({ readStatus: success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts index c97f1c147c454..718587ad15ad5 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { - Doc, + ImportDoc, ImportFailure, ImportResponse, Mappings, @@ -20,6 +20,7 @@ import { const CHUNK_SIZE = 5000; const MAX_CHUNK_CHAR_COUNT = 1000000; const IMPORT_RETRIES = 5; +const STRING_CHUNKS_MB = 100; export interface ImportConfig { settings: Settings; @@ -34,12 +35,19 @@ export interface ImportResults { error?: any; } -export class Importer { +export interface CreateDocsResponse { + success: boolean; + remainder: number; + docs: ImportDoc[]; + error?: any; +} + +export abstract class Importer { private _settings: Settings; private _mappings: Mappings; private _pipeline: IngestPipeline; - protected _docArray: Doc[] = []; + protected _docArray: ImportDoc[] = []; constructor({ settings, mappings, pipeline }: ImportConfig) { this._settings = settings; @@ -47,7 +55,33 @@ export class Importer { this._pipeline = pipeline; } - async initializeImport(index: string) { + public read(data: ArrayBuffer) { + const decoder = new TextDecoder(); + const size = STRING_CHUNKS_MB * Math.pow(2, 20); + + // chop the data up into 100MB chunks for processing. + // if the chop produces a partial line at the end, a character "remainder" count + // is returned which is used to roll the next chunk back that many chars so + // it is included in the next chunk. + const parts = Math.ceil(data.byteLength / size); + let remainder = 0; + for (let i = 0; i < parts; i++) { + const byteArray = decoder.decode(data.slice(i * size - remainder, (i + 1) * size)); + const { success, docs, remainder: tempRemainder } = this._createDocs(byteArray); + if (success) { + this._docArray = this._docArray.concat(docs); + remainder = tempRemainder; + } else { + return { success: false }; + } + } + + return { success: true }; + } + + protected abstract _createDocs(t: string): CreateDocsResponse; + + public async initializeImport(index: string) { const settings = this._settings; const mappings = this._mappings; const pipeline = this._pipeline; @@ -75,7 +109,7 @@ export class Importer { return createIndexResp; } - async import( + public async import( id: string, index: string, pipelineId: string, @@ -201,8 +235,8 @@ function updatePipelineTimezone(ingestPipeline: IngestPipeline) { } } -function createDocumentChunks(docArray: Doc[]) { - const chunks: Doc[][] = []; +function createDocumentChunks(docArray: ImportDoc[]) { + const chunks: ImportDoc[][] = []; // chop docArray into 5000 doc chunks const tempChunks = chunk(docArray, CHUNK_SIZE); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts index 7ccc5a8d673f4..65be24d9e7be4 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Importer, ImportConfig } from './importer'; +import { Importer, ImportConfig, CreateDocsResponse } from './importer'; import { Doc, FindFileStructureResponse, @@ -33,54 +33,54 @@ export class MessageImporter extends Importer { // multiline_start_pattern regex // if it does, it is a legitimate end of line and can be pushed into the list, // if not, it must be a newline char inside a field value, so keep looking. - read(text: string) { + protected _createDocs(text: string): CreateDocsResponse { + let remainder = 0; try { - const data: Doc[] = []; + const docs: Doc[] = []; let message = ''; let line = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; if (char === '\n') { - message = this.processLine(data, message, line); + message = this._processLine(docs, message, line); line = ''; } else { line += char; } } - // the last line may have been missing a newline ending - if (line !== '') { - message = this.processLine(data, message, line); - } + remainder = line.length; - // add the last message to the list if not already done + // // add the last message to the list if not already done if (message !== '') { - this.addMessage(data, message); + this._addMessage(docs, message); } // remove first line if it is blank - if (data[0] && data[0].message === '') { - data.shift(); + if (docs[0] && docs[0].message === '') { + docs.shift(); } - this._docArray = data; - return { success: true, + docs, + remainder, }; } catch (error) { return { success: false, + docs: [], + remainder, error, }; } } - processLine(data: Doc[], message: string, line: string) { + private _processLine(data: Doc[], message: string, line: string) { if (this._excludeLinesRegex === null || line.match(this._excludeLinesRegex) === null) { if (this._multilineStartRegex === null || line.match(this._multilineStartRegex) !== null) { - this.addMessage(data, message); + this._addMessage(data, message); message = ''; } else if (data.length === 0) { // discard everything before the first line that is considered the first line of a message @@ -95,7 +95,7 @@ export class MessageImporter extends Importer { return message; } - addMessage(data: Doc[], message: string) { + private _addMessage(data: Doc[], message: string) { // if the message ended \r\n (Windows line endings) // then omit the \r as well as the \n for consistency message = message.replace(/\r$/, ''); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts index 7f5f37abc5246..17c9de8ef4558 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Importer, ImportConfig } from './importer'; +import { Importer, ImportConfig, CreateDocsResponse } from './importer'; import { FindFileStructureResponse } from '../../../../../../../common/types/file_datavisualizer'; export class NdjsonImporter extends Importer { @@ -12,27 +12,42 @@ export class NdjsonImporter extends Importer { super(settings); } - read(json: string) { + protected _createDocs(json: string): CreateDocsResponse { + let remainder = 0; try { const splitJson = json.split(/}\s*\n/); + const incompleteLastLine = json.match(/}\s*\n?$/) === null; - const ndjson: any[] = []; - for (let i = 0; i < splitJson.length; i++) { - if (splitJson[i] !== '') { - // note the extra } at the end of the line, adding back - // the one that was eaten in the split - ndjson.push(`${splitJson[i]}}`); + const docs: string[] = []; + if (splitJson.length) { + for (let i = 0; i < splitJson.length - 1; i++) { + if (splitJson[i] !== '') { + // note the extra } at the end of the line, adding back + // the one that was eaten in the split + docs.push(`${splitJson[i]}}`); + } } - } - this._docArray = ndjson; + const lastDoc = splitJson[splitJson.length - 1]; + if (lastDoc) { + if (incompleteLastLine === true) { + remainder = lastDoc.length; + } else { + docs.push(`${lastDoc}}`); + } + } + } return { success: true, + docs, + remainder, }; } catch (error) { return { success: false, + docs: [], + remainder, error, }; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts index 0f0036a7c4616..492a797f7a2f2 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts @@ -9,7 +9,6 @@ export { hasImportPermission, processResults, readFile, - reduceData, getMaxBytes, getMaxBytesFormatted, } from './utils'; 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 0d2016b71ed83..7b6464570e55c 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 @@ -9,12 +9,14 @@ import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; import { - ABSOLUTE_MAX_BYTES, + MAX_FILE_SIZE_BYTES, + ABSOLUTE_MAX_FILE_SIZE_BYTES, FILE_SIZE_DISPLAY_FORMAT, } from '../../../../../../common/constants/file_datavisualizer'; import { getMlConfig } from '../../../../util/dependency_cache'; const DEFAULT_LINES_TO_SAMPLE = 1000; +const UPLOAD_SIZE_MB = 5; const overrideDefaults = { timestampFormat: undefined, @@ -34,15 +36,22 @@ export function readFile(file: File) { return new Promise((resolve, reject) => { if (file && file.size) { const reader = new FileReader(); - reader.readAsText(file); + reader.readAsArrayBuffer(file); reader.onload = (() => { return () => { + const decoder = new TextDecoder(); const data = reader.result; - if (data === '') { + if (data === null || typeof data === 'string') { + return reject(); + } + const size = UPLOAD_SIZE_MB * Math.pow(2, 20); + const fileContents = decoder.decode(data.slice(0, size)); + + if (fileContents === '') { reject(); } else { - resolve({ data }); + resolve({ fileContents, data }); } }; })(); @@ -52,17 +61,14 @@ export function readFile(file: File) { }); } -export function reduceData(data: string, mb: number) { - // assuming ascii characters in the file where 1 char is 1 byte - // TODO - change this when other non UTF-8 formats are - // supported for the read data - const size = mb * Math.pow(2, 20); - return data.length >= size ? data.slice(0, size) : data; -} - export function getMaxBytes() { - const maxBytes = getMlConfig().file_data_visualizer.max_file_size_bytes; - return maxBytes < ABSOLUTE_MAX_BYTES ? maxBytes : ABSOLUTE_MAX_BYTES; + const maxFileSize = getMlConfig().file_data_visualizer.max_file_size; + // @ts-ignore + const maxBytes = numeral(maxFileSize.toUpperCase()).value(); + if (maxBytes < MAX_FILE_SIZE_BYTES) { + return MAX_FILE_SIZE_BYTES; + } + return maxBytes < ABSOLUTE_MAX_FILE_SIZE_BYTES ? maxBytes : ABSOLUTE_MAX_FILE_SIZE_BYTES; } export function getMaxBytesFormatted() { diff --git a/x-pack/plugins/ml/public/application/util/__tests__/calc_auto_interval.js b/x-pack/plugins/ml/public/application/util/__tests__/calc_auto_interval.js deleted file mode 100644 index 0553cec5cd7d4..0000000000000 --- a/x-pack/plugins/ml/public/application/util/__tests__/calc_auto_interval.js +++ /dev/null @@ -1,140 +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 expect from '@kbn/expect'; -import moment from 'moment'; - -import { timeBucketsCalcAutoIntervalProvider } from '../calc_auto_interval'; - -describe('ML - calc auto intervals', () => { - const calcAuto = timeBucketsCalcAutoIntervalProvider(); - - describe('near interval', () => { - it('returns 0ms buckets for undefined / 0 bars', () => { - const interval = calcAuto.near(0, undefined); - expect(interval.asMilliseconds()).to.be(0); - }); - - it('returns 1000ms buckets for 60s / 100 bars', () => { - const interval = calcAuto.near(100, moment.duration(60, 's')); - expect(interval.asMilliseconds()).to.be(1000); - }); - - it('returns 5m buckets for 8h / 100 bars', () => { - const interval = calcAuto.near(100, moment.duration(8, 'h')); - expect(interval.asMinutes()).to.be(5); - }); - - it('returns 15m buckets for 1d / 100 bars', () => { - const interval = calcAuto.near(100, moment.duration(1, 'd')); - expect(interval.asMinutes()).to.be(15); - }); - - it('returns 1h buckets for 20d / 500 bars', () => { - const interval = calcAuto.near(500, moment.duration(20, 'd')); - expect(interval.asHours()).to.be(1); - }); - - it('returns 6h buckets for 100d / 500 bars', () => { - const interval = calcAuto.near(500, moment.duration(100, 'd')); - expect(interval.asHours()).to.be(6); - }); - - it('returns 24h buckets for 1y / 500 bars', () => { - const interval = calcAuto.near(500, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(24); - }); - - it('returns 12h buckets for 1y / 1000 bars', () => { - const interval = calcAuto.near(1000, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(12); - }); - }); - - describe('lessThan interval', () => { - it('returns 0ms buckets for undefined / 0 bars', () => { - const interval = calcAuto.lessThan(0, undefined); - expect(interval.asMilliseconds()).to.be(0); - }); - - it('returns 500ms buckets for 60s / 100 bars', () => { - const interval = calcAuto.lessThan(100, moment.duration(60, 's')); - expect(interval.asMilliseconds()).to.be(500); - }); - - it('returns 5m buckets for 8h / 100 bars', () => { - const interval = calcAuto.lessThan(100, moment.duration(8, 'h')); - expect(interval.asMinutes()).to.be(5); - }); - - it('returns 30m buckets for 1d / 100 bars', () => { - const interval = calcAuto.lessThan(100, moment.duration(1, 'd')); - expect(interval.asMinutes()).to.be(30); - }); - - it('returns 1h buckets for 20d / 500 bars', () => { - const interval = calcAuto.lessThan(500, moment.duration(20, 'd')); - expect(interval.asHours()).to.be(1); - }); - - it('returns 6h buckets for 100d / 500 bars', () => { - const interval = calcAuto.lessThan(500, moment.duration(100, 'd')); - expect(interval.asHours()).to.be(6); - }); - - it('returns 24h buckets for 1y / 500 bars', () => { - const interval = calcAuto.lessThan(500, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(24); - }); - - it('returns 12h buckets for 1y / 1000 bars', () => { - const interval = calcAuto.lessThan(1000, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(12); - }); - }); - - describe('atLeast interval', () => { - it('returns 0ms buckets for undefined / 0 bars', () => { - const interval = calcAuto.atLeast(0, undefined); - expect(interval.asMilliseconds()).to.be(0); - }); - - it('returns 100ms buckets for 60s / 100 bars', () => { - const interval = calcAuto.atLeast(100, moment.duration(60, 's')); - expect(interval.asMilliseconds()).to.be(100); - }); - - it('returns 1m buckets for 8h / 100 bars', () => { - const interval = calcAuto.atLeast(100, moment.duration(8, 'h')); - expect(interval.asMinutes()).to.be(1); - }); - - it('returns 10m buckets for 1d / 100 bars', () => { - const interval = calcAuto.atLeast(100, moment.duration(1, 'd')); - expect(interval.asMinutes()).to.be(10); - }); - - it('returns 30m buckets for 20d / 500 bars', () => { - const interval = calcAuto.atLeast(500, moment.duration(20, 'd')); - expect(interval.asMinutes()).to.be(30); - }); - - it('returns 4h buckets for 100d / 500 bars', () => { - const interval = calcAuto.atLeast(500, moment.duration(100, 'd')); - expect(interval.asHours()).to.be(4); - }); - - it('returns 12h buckets for 1y / 500 bars', () => { - const interval = calcAuto.atLeast(500, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(12); - }); - - it('returns 8h buckets for 1y / 1000 bars', () => { - const interval = calcAuto.atLeast(1000, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(8); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/util/__tests__/chart_utils.js b/x-pack/plugins/ml/public/application/util/__tests__/chart_utils.js deleted file mode 100644 index 89df5946abe76..0000000000000 --- a/x-pack/plugins/ml/public/application/util/__tests__/chart_utils.js +++ /dev/null @@ -1,297 +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 $ from 'jquery'; -import d3 from 'd3'; -import expect from '@kbn/expect'; -import { - chartLimits, - filterAxisLabels, - getChartType, - numTicks, - showMultiBucketAnomalyMarker, - showMultiBucketAnomalyTooltip, -} from '../chart_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; -import { CHART_TYPE } from '../../explorer/explorer_constants'; - -describe('ML - chart utils', () => { - describe('chartLimits', () => { - it('returns NaN when called without data', () => { - const limits = chartLimits(); - expect(limits.min).to.be.NaN; - expect(limits.max).to.be.NaN; - }); - - it('returns {max: 625736376, min: 201039318} for some test data', () => { - const data = [ - { - date: new Date('2017-02-23T08:00:00.000Z'), - value: 228243469, - anomalyScore: 63.32916, - numberOfCauses: 1, - actual: [228243469], - typical: [133107.7703441773], - }, - { date: new Date('2017-02-23T09:00:00.000Z'), value: null }, - { date: new Date('2017-02-23T10:00:00.000Z'), value: null }, - { date: new Date('2017-02-23T11:00:00.000Z'), value: null }, - { - date: new Date('2017-02-23T12:00:00.000Z'), - value: 625736376, - anomalyScore: 97.32085, - numberOfCauses: 1, - actual: [625736376], - typical: [132830.424736973], - }, - { - date: new Date('2017-02-23T13:00:00.000Z'), - value: 201039318, - anomalyScore: 59.83488, - numberOfCauses: 1, - actual: [201039318], - typical: [132739.5267403542], - }, - ]; - - const limits = chartLimits(data); - - // {max: 625736376, min: 201039318} - expect(limits.min).to.be(201039318); - expect(limits.max).to.be(625736376); - }); - - it("adds 5% padding when min/max are the same, e.g. when there's only one data point", () => { - const data = [ - { - date: new Date('2017-02-23T08:00:00.000Z'), - value: 100, - anomalyScore: 50, - numberOfCauses: 1, - actual: [100], - typical: [100], - }, - ]; - - const limits = chartLimits(data); - expect(limits.min).to.be(95); - expect(limits.max).to.be(105); - }); - - it('returns minimum of 0 when data includes an anomaly for missing data', () => { - const data = [ - { date: new Date('2017-02-23T09:00:00.000Z'), value: 22.2 }, - { date: new Date('2017-02-23T10:00:00.000Z'), value: 23.3 }, - { date: new Date('2017-02-23T11:00:00.000Z'), value: 24.4 }, - { - date: new Date('2017-02-23T12:00:00.000Z'), - value: null, - anomalyScore: 97.32085, - actual: [0], - typical: [22.2], - }, - { date: new Date('2017-02-23T13:00:00.000Z'), value: 21.3 }, - { date: new Date('2017-02-23T14:00:00.000Z'), value: 21.2 }, - { date: new Date('2017-02-23T15:00:00.000Z'), value: 21.1 }, - ]; - - const limits = chartLimits(data); - expect(limits.min).to.be(0); - expect(limits.max).to.be(24.4); - }); - }); - - describe('filterAxisLabels', () => { - it('throws an error when called without arguments', () => { - expect(() => filterAxisLabels()).to.throwError(); - }); - - it('filters axis labels', () => { - // this provides a dummy structure of axis labels. - // the first one should always be filtered because it overflows on the - // left side of the axis. the last one should be filtered based on the - // given width parameter when doing the test calls. - $('body').append(` - - - - 06:00 - - - 12:00 - - - 18:00 - - - 00:00 - - - - `); - - const selector = '#filterAxisLabels .x.axis'; - - // given this width, the last tick should not be removed - filterAxisLabels(d3.selectAll(selector), 1000); - expect(d3.selectAll(selector + ' .tick text').size()).to.be(3); - - // given this width, the last tick should be removed - filterAxisLabels(d3.selectAll(selector), 790); - expect(d3.selectAll(selector + ' .tick text').size()).to.be(2); - - // clean up - $('#filterAxisLabels').remove(); - }); - }); - - describe('getChartType', () => { - const singleMetricConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'responsetime', - entityFields: [], - }; - - const multiMetricConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'responsetime', - entityFields: [ - { - fieldName: 'airline', - fieldValue: 'AAL', - fieldType: 'partition', - }, - ], - }; - - const populationConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'http.response.body.bytes', - entityFields: [ - { - fieldName: 'source.ip', - fieldValue: '10.11.12.13', - fieldType: 'over', - }, - ], - }; - - const rareConfig = { - metricFunction: 'count', - functionDescription: 'rare', - entityFields: [ - { - fieldName: 'http.response.status_code', - fieldValue: '404', - fieldType: 'by', - }, - ], - }; - - const varpModelPlotConfig = { - metricFunction: null, - functionDescription: 'varp', - fieldName: 'NetworkOut', - entityFields: [ - { - fieldName: 'instance', - fieldValue: 'i-ef74d410', - fieldType: 'over', - }, - ], - }; - - const overScriptFieldModelPlotConfig = { - metricFunction: 'count', - functionDescription: 'count', - fieldName: 'highest_registered_domain', - entityFields: [ - { - fieldName: 'highest_registered_domain', - fieldValue: 'elastic.co', - fieldType: 'over', - }, - ], - datafeedConfig: { - script_fields: { - highest_registered_domain: { - script: { - source: "return domainSplit(doc['query'].value, params).get(1);", - lang: 'painless', - }, - ignore_failure: false, - }, - }, - }, - }; - - it('returns single metric chart type as expected for configs', () => { - expect(getChartType(singleMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(multiMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(varpModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(overScriptFieldModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - }); - - it('returns event distribution chart type as expected for configs', () => { - expect(getChartType(rareConfig)).to.be(CHART_TYPE.EVENT_DISTRIBUTION); - }); - - it('returns population distribution chart type as expected for configs', () => { - expect(getChartType(populationConfig)).to.be(CHART_TYPE.POPULATION_DISTRIBUTION); - }); - }); - - describe('numTicks', () => { - it('returns 10 for 1000', () => { - expect(numTicks(1000)).to.be(10); - }); - }); - - describe('showMultiBucketAnomalyMarker', () => { - it('returns true for points with multiBucketImpact at or above medium impact', () => { - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be( - true - ); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be( - true - ); - }); - - it('returns false for points with multiBucketImpact missing or below medium impact', () => { - expect(showMultiBucketAnomalyMarker({})).to.be(false); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be( - false - ); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be( - false - ); - }); - }); - - describe('showMultiBucketAnomalyTooltip', () => { - it('returns true for points with multiBucketImpact at or above low impact', () => { - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be( - true - ); - expect( - showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM }) - ).to.be(true); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be( - true - ); - }); - - it('returns false for points with multiBucketImpact missing or below medium impact', () => { - expect(showMultiBucketAnomalyTooltip({})).to.be(false); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be( - false - ); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/util/__tests__/string_utils.js b/x-pack/plugins/ml/public/application/util/__tests__/string_utils.js deleted file mode 100644 index 702e9dfd96205..0000000000000 --- a/x-pack/plugins/ml/public/application/util/__tests__/string_utils.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 expect from '@kbn/expect'; -import { - replaceStringTokens, - detectorToString, - sortByKey, - guessTimeFormat, - toLocaleString, - mlEscape, - escapeForElasticsearchQuery, -} from '../string_utils'; - -describe('ML - string utils', () => { - describe('replaceStringTokens', () => { - const testRecord = { - job_id: 'test_job', - result_type: 'record', - probability: 0.0191711, - record_score: 4.3, - bucket_span: 300, - detector_index: 0, - timestamp: 1454890500000, - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - user: "Des O'Connor", - testfield1: 'test$tring=[+-?]', - testfield2: '{<()>}', - testfield3: 'host=\\\\test@uk.dev', - }; - - it('returns correct values without URI encoding', () => { - const result = replaceStringTokens('user=$user$,time=$timestamp$', testRecord, false); - expect(result).to.be("user=Des O'Connor,time=1454890500000"); - }); - - it('returns correct values for missing token without URI encoding', () => { - const result = replaceStringTokens('user=$username$,time=$timestamp$', testRecord, false); - expect(result).to.be('user=$username$,time=1454890500000'); - }); - - it('returns correct values with URI encoding', () => { - const testString1 = 'https://www.google.co.uk/webhp#q=$testfield1$'; - const testString2 = 'https://www.google.co.uk/webhp#q=$testfield2$'; - const testString3 = 'https://www.google.co.uk/webhp#q=$testfield3$'; - const testString4 = 'https://www.google.co.uk/webhp#q=$user$'; - - const result1 = replaceStringTokens(testString1, testRecord, true); - const result2 = replaceStringTokens(testString2, testRecord, true); - const result3 = replaceStringTokens(testString3, testRecord, true); - const result4 = replaceStringTokens(testString4, testRecord, true); - - expect(result1).to.be('https://www.google.co.uk/webhp#q=test%24tring%3D%5B%2B-%3F%5D'); - expect(result2).to.be('https://www.google.co.uk/webhp#q=%7B%3C()%3E%7D'); - expect(result3).to.be('https://www.google.co.uk/webhp#q=host%3D%5C%5Ctest%40uk.dev'); - expect(result4).to.be("https://www.google.co.uk/webhp#q=Des%20O'Connor"); - }); - - it('returns correct values for missing token with URI encoding', () => { - const testString = 'https://www.google.co.uk/webhp#q=$username$&time=$timestamp$'; - const result = replaceStringTokens(testString, testRecord, true); - expect(result).to.be('https://www.google.co.uk/webhp#q=$username$&time=1454890500000'); - }); - }); - - describe('detectorToString', () => { - it('returns the correct descriptions for detectors', () => { - const detector1 = { - function: 'count', - }; - - const detector2 = { - function: 'count', - by_field_name: 'airline', - use_null: false, - }; - - const detector3 = { - function: 'mean', - field_name: 'CPUUtilization', - partition_field_name: 'region', - by_field_name: 'host', - over_field_name: 'user', - exclude_frequent: 'all', - }; - - expect(detectorToString(detector1)).to.be('count'); - expect(detectorToString(detector2)).to.be('count by airline use_null=false'); - expect(detectorToString(detector3)).to.be( - 'mean(CPUUtilization) by host over user partition_field_name=region exclude_frequent=all' - ); - }); - }); - - describe('sortByKey', () => { - const obj = { - zebra: 'stripes', - giraffe: 'neck', - elephant: 'trunk', - }; - - const valueComparator = function(value) { - return value; - }; - - it('returns correct ordering with default comparator', () => { - const result = sortByKey(obj, false); - const keys = Object.keys(result); - expect(keys[0]).to.be('elephant'); - expect(keys[1]).to.be('giraffe'); - expect(keys[2]).to.be('zebra'); - }); - - it('returns correct ordering with default comparator and order reversed', () => { - const result = sortByKey(obj, true); - const keys = Object.keys(result); - expect(keys[0]).to.be('zebra'); - expect(keys[1]).to.be('giraffe'); - expect(keys[2]).to.be('elephant'); - }); - - it('returns correct ordering with comparator', () => { - const result = sortByKey(obj, false, valueComparator); - const keys = Object.keys(result); - expect(keys[0]).to.be('giraffe'); - expect(keys[1]).to.be('zebra'); - expect(keys[2]).to.be('elephant'); - }); - - it('returns correct ordering with comparator and order reversed', () => { - const result = sortByKey(obj, true, valueComparator); - const keys = Object.keys(result); - expect(keys[0]).to.be('elephant'); - expect(keys[1]).to.be('zebra'); - expect(keys[2]).to.be('giraffe'); - }); - }); - - describe('guessTimeFormat', () => { - it('returns correct format for various dates', () => { - expect(guessTimeFormat('2017-03-24T00:00')).to.be("yyyy-MM-dd'T'HH:mm"); - expect(guessTimeFormat('2017-03-24 00:00')).to.be('yyyy-MM-dd HH:mm'); - expect(guessTimeFormat('2017-03-24 00:00:00')).to.be('yyyy-MM-dd HH:mm:ss'); - expect(guessTimeFormat('2017-03-24 00:00:00Z')).to.be('yyyy-MM-dd HH:mm:ssX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000')).to.be('yyyy-MM-dd HH:mm:ss.SSS'); - expect(guessTimeFormat('2017-03-24 00:00:00:000')).to.be('yyyy-MM-dd HH:mm:ss:SSS'); - expect(guessTimeFormat('2017-03-24 00:00:00.000+00:00:00')).to.be( - 'yyyy-MM-dd HH:mm:ss.SSSXXXXX' - ); - expect(guessTimeFormat('2017-03-24 00:00:00.000+00:00')).to.be('yyyy-MM-dd HH:mm:ss.SSSXXX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000+000000')).to.be( - 'yyyy-MM-dd HH:mm:ss.SSSXXXX' - ); - expect(guessTimeFormat('2017-03-24 00:00:00.000+0000')).to.be('yyyy-MM-dd HH:mm:ss.SSSZ'); - expect(guessTimeFormat('2017-03-24 00:00:00.000+00')).to.be('yyyy-MM-dd HH:mm:ss.SSSX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000Z')).to.be('yyyy-MM-dd HH:mm:ss.SSSX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000 GMT')).to.be('yyyy-MM-dd HH:mm:ss.SSS zzz'); - expect(guessTimeFormat('2017-03-24 00:00:00 GMT')).to.be('yyyy-MM-dd HH:mm:ss zzz'); - expect(guessTimeFormat('2017 03 24 00:00:00.000')).to.be('yyyy MM dd HH:mm:ss.SSS'); - expect(guessTimeFormat('2017.03.24 00:00:00.000')).to.be('yyyy.MM.dd HH:mm:ss.SSS'); - expect(guessTimeFormat('2017/03/24 00:00:00.000')).to.be('yyyy/MM/dd HH:mm:ss.SSS'); - expect(guessTimeFormat('24/03/2017 00:00:00.000')).to.be('dd/MM/yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('03 24 2017 00:00:00.000')).to.be('MM dd yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('03/24/2017 00:00:00.000')).to.be('MM/dd/yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('2017 Mar 24 00:00:00.000')).to.be('yyyy MMM dd HH:mm:ss.SSS'); - expect(guessTimeFormat('Mar 24 2017 00:00:00.000')).to.be('MMM dd yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('24 Mar 2017 00:00:00.000')).to.be('dd MMM yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('1490313600')).to.be('epoch'); - expect(guessTimeFormat('1490313600000')).to.be('epoch_ms'); - }); - }); - - describe('toLocaleString', () => { - it('returns correct comma placement for large numbers', () => { - expect(toLocaleString(1)).to.be('1'); - expect(toLocaleString(10)).to.be('10'); - expect(toLocaleString(100)).to.be('100'); - expect(toLocaleString(1000)).to.be('1,000'); - expect(toLocaleString(10000)).to.be('10,000'); - expect(toLocaleString(100000)).to.be('100,000'); - expect(toLocaleString(1000000)).to.be('1,000,000'); - expect(toLocaleString(10000000)).to.be('10,000,000'); - expect(toLocaleString(100000000)).to.be('100,000,000'); - expect(toLocaleString(1000000000)).to.be('1,000,000,000'); - }); - }); - - describe('mlEscape', () => { - it('returns correct escaping of characters', () => { - expect(mlEscape('foo&bar')).to.be('foo&bar'); - expect(mlEscape('foobar')).to.be('foo>bar'); - expect(mlEscape('foo"bar')).to.be('foo"bar'); - expect(mlEscape("foo'bar")).to.be('foo'bar'); - expect(mlEscape('foo/bar')).to.be('foo/bar'); - }); - }); - - describe('escapeForElasticsearchQuery', () => { - it('returns correct escaping of reserved elasticsearch characters', () => { - expect(escapeForElasticsearchQuery('foo+bar')).to.be('foo\\+bar'); - expect(escapeForElasticsearchQuery('foo-bar')).to.be('foo\\-bar'); - expect(escapeForElasticsearchQuery('foo=bar')).to.be('foo\\=bar'); - expect(escapeForElasticsearchQuery('foo&&bar')).to.be('foo\\&\\&bar'); - expect(escapeForElasticsearchQuery('foo||bar')).to.be('foo\\|\\|bar'); - expect(escapeForElasticsearchQuery('foo>bar')).to.be('foo\\>bar'); - expect(escapeForElasticsearchQuery('foo { + const calcAuto = timeBucketsCalcAutoIntervalProvider(); + + describe('near interval', () => { + test('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.near(0, undefined); + expect(interval.asMilliseconds()).toBe(0); + }); + + test('returns 1000ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).toBe(1000); + }); + + test('returns 5m buckets for 8h / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).toBe(5); + }); + + test('returns 15m buckets for 1d / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).toBe(15); + }); + + test('returns 1h buckets for 20d / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(20, 'd')); + expect(interval.asHours()).toBe(1); + }); + + test('returns 6h buckets for 100d / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(100, 'd')); + expect(interval.asHours()).toBe(6); + }); + + test('returns 24h buckets for 1y / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(24); + }); + + test('returns 12h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.near(1000, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(12); + }); + }); + + describe('lessThan interval', () => { + test('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.lessThan(0, undefined); + expect(interval.asMilliseconds()).toBe(0); + }); + + test('returns 500ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).toBe(500); + }); + + test('returns 5m buckets for 8h / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).toBe(5); + }); + + test('returns 30m buckets for 1d / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).toBe(30); + }); + + test('returns 1h buckets for 20d / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(20, 'd')); + expect(interval.asHours()).toBe(1); + }); + + test('returns 6h buckets for 100d / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(100, 'd')); + expect(interval.asHours()).toBe(6); + }); + + test('returns 24h buckets for 1y / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(24); + }); + + test('returns 12h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.lessThan(1000, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(12); + }); + }); + + describe('atLeast interval', () => { + test('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.atLeast(0, undefined); + expect(interval.asMilliseconds()).toBe(0); + }); + + test('returns 100ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).toBe(100); + }); + + test('returns 1m buckets for 8h / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).toBe(1); + }); + + test('returns 10m buckets for 1d / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).toBe(10); + }); + + test('returns 30m buckets for 20d / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(20, 'd')); + expect(interval.asMinutes()).toBe(30); + }); + + test('returns 4h buckets for 100d / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(100, 'd')); + expect(interval.asHours()).toBe(4); + }); + + test('returns 12h buckets for 1y / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(12); + }); + + test('returns 8h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.atLeast(1000, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(8); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/plugins/ml/public/application/util/chart_utils.test.js index 4b33cb131be7f..57aea3c0ab5aa 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.test.js @@ -29,246 +29,488 @@ const timefilter = getTimefilter(); import d3 from 'd3'; import moment from 'moment'; -import { mount } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { + chartLimits, + getChartType, getExploreSeriesLink, getTickValues, - isLabelLengthAboveThreshold, getXTransform, + isLabelLengthAboveThreshold, + numTicks, removeLabelOverlap, + showMultiBucketAnomalyMarker, + showMultiBucketAnomalyTooltip, } from './chart_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; +import { CHART_TYPE } from '../explorer/explorer_constants'; + timefilter.setTime({ from: moment(seriesConfig.selectedEarliest).toISOString(), to: moment(seriesConfig.selectedLatest).toISOString(), }); -describe('getExploreSeriesLink', () => { - test('get timeseriesexplorer link', () => { - const link = getExploreSeriesLink(seriesConfig); - const expectedLink = - `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + - `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + - `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + - `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + - `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`; - - expect(link).toBe(expectedLink); - }); -}); +describe('ML - chart utils', () => { + describe('chartLimits', () => { + test('returns NaN when called without data', () => { + const limits = chartLimits(); + expect(limits.min).toBeNaN(); + expect(limits.max).toBeNaN(); + }); -describe('getTickValues', () => { - test('farequote sample data', () => { - const tickValues = getTickValues(1486656000000, 14400000, 1486606500000, 1486719900000); - - expect(tickValues).toEqual([ - 1486612800000, - 1486627200000, - 1486641600000, - 1486656000000, - 1486670400000, - 1486684800000, - 1486699200000, - 1486713600000, - ]); - }); + test('returns {max: 625736376, min: 201039318} for some test data', () => { + const data = [ + { + date: new Date('2017-02-23T08:00:00.000Z'), + value: 228243469, + anomalyScore: 63.32916, + numberOfCauses: 1, + actual: [228243469], + typical: [133107.7703441773], + }, + { date: new Date('2017-02-23T09:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T10:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T11:00:00.000Z'), value: null }, + { + date: new Date('2017-02-23T12:00:00.000Z'), + value: 625736376, + anomalyScore: 97.32085, + numberOfCauses: 1, + actual: [625736376], + typical: [132830.424736973], + }, + { + date: new Date('2017-02-23T13:00:00.000Z'), + value: 201039318, + anomalyScore: 59.83488, + numberOfCauses: 1, + actual: [201039318], + typical: [132739.5267403542], + }, + ]; + + const limits = chartLimits(data); + + // {max: 625736376, min: 201039318} + expect(limits.min).toBe(201039318); + expect(limits.max).toBe(625736376); + }); - test('filebeat sample data', () => { - const tickValues = getTickValues(1486080000000, 14400000, 1485860400000, 1486314000000); - expect(tickValues).toEqual([ - 1485864000000, - 1485878400000, - 1485892800000, - 1485907200000, - 1485921600000, - 1485936000000, - 1485950400000, - 1485964800000, - 1485979200000, - 1485993600000, - 1486008000000, - 1486022400000, - 1486036800000, - 1486051200000, - 1486065600000, - 1486080000000, - 1486094400000, - 1486108800000, - 1486123200000, - 1486137600000, - 1486152000000, - 1486166400000, - 1486180800000, - 1486195200000, - 1486209600000, - 1486224000000, - 1486238400000, - 1486252800000, - 1486267200000, - 1486281600000, - 1486296000000, - 1486310400000, - ]); + test("adds 5% padding when min/max are the same, e.g. when there's only one data point", () => { + const data = [ + { + date: new Date('2017-02-23T08:00:00.000Z'), + value: 100, + anomalyScore: 50, + numberOfCauses: 1, + actual: [100], + typical: [100], + }, + ]; + + const limits = chartLimits(data); + expect(limits.min).toBe(95); + expect(limits.max).toBe(105); + }); + + test('returns minimum of 0 when data includes an anomaly for missing data', () => { + const data = [ + { date: new Date('2017-02-23T09:00:00.000Z'), value: 22.2 }, + { date: new Date('2017-02-23T10:00:00.000Z'), value: 23.3 }, + { date: new Date('2017-02-23T11:00:00.000Z'), value: 24.4 }, + { + date: new Date('2017-02-23T12:00:00.000Z'), + value: null, + anomalyScore: 97.32085, + actual: [0], + typical: [22.2], + }, + { date: new Date('2017-02-23T13:00:00.000Z'), value: 21.3 }, + { date: new Date('2017-02-23T14:00:00.000Z'), value: 21.2 }, + { date: new Date('2017-02-23T15:00:00.000Z'), value: 21.1 }, + ]; + + const limits = chartLimits(data); + expect(limits.min).toBe(0); + expect(limits.max).toBe(24.4); + }); }); - test('gallery sample data', () => { - const tickValues = getTickValues(1518652800000, 604800000, 1518274800000, 1519635600000); - expect(tickValues).toEqual([1518652800000, 1519257600000]); + describe('getChartType', () => { + const singleMetricConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'responsetime', + entityFields: [], + }; + + const multiMetricConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'responsetime', + entityFields: [ + { + fieldName: 'airline', + fieldValue: 'AAL', + fieldType: 'partition', + }, + ], + }; + + const populationConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'http.response.body.bytes', + entityFields: [ + { + fieldName: 'source.ip', + fieldValue: '10.11.12.13', + fieldType: 'over', + }, + ], + }; + + const rareConfig = { + metricFunction: 'count', + functionDescription: 'rare', + entityFields: [ + { + fieldName: 'http.response.status_code', + fieldValue: '404', + fieldType: 'by', + }, + ], + }; + + const varpModelPlotConfig = { + metricFunction: null, + functionDescription: 'varp', + fieldName: 'NetworkOut', + entityFields: [ + { + fieldName: 'instance', + fieldValue: 'i-ef74d410', + fieldType: 'over', + }, + ], + }; + + const overScriptFieldModelPlotConfig = { + metricFunction: 'count', + functionDescription: 'count', + fieldName: 'highest_registered_domain', + entityFields: [ + { + fieldName: 'highest_registered_domain', + fieldValue: 'elastic.co', + fieldType: 'over', + }, + ], + datafeedConfig: { + script_fields: { + highest_registered_domain: { + script: { + source: "return domainSplit(doc['query'].value, params).get(1);", + lang: 'painless', + }, + ignore_failure: false, + }, + }, + }, + }; + + test('returns single metric chart type as expected for configs', () => { + expect(getChartType(singleMetricConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(multiMetricConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(varpModelPlotConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(overScriptFieldModelPlotConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + }); + + test('returns event distribution chart type as expected for configs', () => { + expect(getChartType(rareConfig)).toBe(CHART_TYPE.EVENT_DISTRIBUTION); + }); + + test('returns population distribution chart type as expected for configs', () => { + expect(getChartType(populationConfig)).toBe(CHART_TYPE.POPULATION_DISTRIBUTION); + }); }); - test('invalid tickIntervals trigger an error', () => { - expect(() => { - getTickValues(1518652800000, 0, 1518274800000, 1519635600000); - }).toThrow(); - expect(() => { - getTickValues(1518652800000, -604800000, 1518274800000, 1519635600000); - }).toThrow(); + describe('getExploreSeriesLink', () => { + test('get timeseriesexplorer link', () => { + const link = getExploreSeriesLink(seriesConfig); + const expectedLink = + `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + + `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + + `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + + `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + + `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`; + + expect(link).toBe(expectedLink); + }); }); -}); -describe('isLabelLengthAboveThreshold', () => { - test('short label', () => { - const isLongLabel = isLabelLengthAboveThreshold({ - detectorLabel: 'count', - entityFields: seriesConfig.entityFields, + describe('numTicks', () => { + test('returns 10 for 1000', () => { + expect(numTicks(1000)).toBe(10); }); - expect(isLongLabel).toBeFalsy(); }); - test('long label', () => { - const isLongLabel = isLabelLengthAboveThreshold(seriesConfig); - expect(isLongLabel).toBeTruthy(); + describe('showMultiBucketAnomalyMarker', () => { + test('returns true for points with multiBucketImpact at or above medium impact', () => { + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).toBe( + true + ); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).toBe( + true + ); + }); + + test('returns false for points with multiBucketImpact missing or below medium impact', () => { + expect(showMultiBucketAnomalyMarker({})).toBe(false); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).toBe( + false + ); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).toBe( + false + ); + }); }); -}); -describe('getXTransform', () => { - const expectedXTransform = 0.007167499999999999; + describe('showMultiBucketAnomalyTooltip', () => { + test('returns true for points with multiBucketImpact at or above low impact', () => { + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).toBe( + true + ); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).toBe( + true + ); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).toBe( + true + ); + }); - test('Chrome/Safari/Firefox String variant.', () => { - const transformStr = 'translate(0.007167499999999999,0)'; - const xTransform = getXTransform(transformStr); - expect(xTransform).toEqual(expectedXTransform); + test('returns false for points with multiBucketImpact missing or below medium impact', () => { + expect(showMultiBucketAnomalyTooltip({})).toBe(false); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).toBe( + false + ); + }); }); - test('IE11 String variant.', () => { - const transformStr = 'translate(0.007167499999999999)'; - const xTransform = getXTransform(transformStr); - expect(xTransform).toEqual(expectedXTransform); + describe('getTickValues', () => { + test('farequote sample data', () => { + const tickValues = getTickValues(1486656000000, 14400000, 1486606500000, 1486719900000); + + expect(tickValues).toEqual([ + 1486612800000, + 1486627200000, + 1486641600000, + 1486656000000, + 1486670400000, + 1486684800000, + 1486699200000, + 1486713600000, + ]); + }); + + test('filebeat sample data', () => { + const tickValues = getTickValues(1486080000000, 14400000, 1485860400000, 1486314000000); + expect(tickValues).toEqual([ + 1485864000000, + 1485878400000, + 1485892800000, + 1485907200000, + 1485921600000, + 1485936000000, + 1485950400000, + 1485964800000, + 1485979200000, + 1485993600000, + 1486008000000, + 1486022400000, + 1486036800000, + 1486051200000, + 1486065600000, + 1486080000000, + 1486094400000, + 1486108800000, + 1486123200000, + 1486137600000, + 1486152000000, + 1486166400000, + 1486180800000, + 1486195200000, + 1486209600000, + 1486224000000, + 1486238400000, + 1486252800000, + 1486267200000, + 1486281600000, + 1486296000000, + 1486310400000, + ]); + }); + + test('gallery sample data', () => { + const tickValues = getTickValues(1518652800000, 604800000, 1518274800000, 1519635600000); + expect(tickValues).toEqual([1518652800000, 1519257600000]); + }); + + test('invalid tickIntervals trigger an error', () => { + expect(() => { + getTickValues(1518652800000, 0, 1518274800000, 1519635600000); + }).toThrow(); + expect(() => { + getTickValues(1518652800000, -604800000, 1518274800000, 1519635600000); + }).toThrow(); + }); }); - test('Invalid String.', () => { - const transformStr = 'translate()'; - const xTransform = getXTransform(transformStr); - expect(xTransform).toEqual(NaN); + describe('isLabelLengthAboveThreshold', () => { + test('short label', () => { + const isLongLabel = isLabelLengthAboveThreshold({ + detectorLabel: 'count', + entityFields: seriesConfig.entityFields, + }); + expect(isLongLabel).toBeFalsy(); + }); + + test('long label', () => { + const isLongLabel = isLabelLengthAboveThreshold(seriesConfig); + expect(isLongLabel).toBeTruthy(); + }); }); -}); -describe('removeLabelOverlap', () => { - const originalGetBBox = SVGElement.prototype.getBBox; - - // This resembles how ExplorerChart renders its x axis. - // We set up this boilerplate so we can then run removeLabelOverlap() - // on some "real" structure. - function axisSetup({ interval, plotEarliest, plotLatest, startTimeMs, xAxisTickFormat }) { - const wrapper = mount(
); - const node = wrapper.getDOMNode(); - - const chartHeight = 170; - const margin = { top: 10, right: 0, bottom: 30, left: 60 }; - const svgWidth = 500; - const svgHeight = chartHeight + margin.top + margin.bottom; - const vizWidth = 500; - - const chartElement = d3.select(node); - - const lineChartXScale = d3.time - .scale() - .range([0, vizWidth]) - .domain([plotEarliest, plotLatest]); - - const xAxis = d3.svg - .axis() - .scale(lineChartXScale) - .orient('bottom') - .innerTickSize(-chartHeight) - .outerTickSize(0) - .tickPadding(10) - .tickFormat(d => moment(d).format(xAxisTickFormat)); - - const tickValues = getTickValues(startTimeMs, interval, plotEarliest, plotLatest); - xAxis.tickValues(tickValues); - - const svg = chartElement - .append('svg') - .attr('width', svgWidth) - .attr('height', svgHeight); - - const axes = svg.append('g'); - - const gAxis = axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + chartHeight + ')') - .call(xAxis); - - return { - gAxis, - node, - vizWidth, - }; - } + describe('getXTransform', () => { + const expectedXTransform = 0.007167499999999999; - test('farequote sample data', () => { - const mockedGetBBox = { width: 27.21875 }; - SVGElement.prototype.getBBox = () => mockedGetBBox; + test('Chrome/Safari/Firefox String variant.', () => { + const transformStr = 'translate(0.007167499999999999,0)'; + const xTransform = getXTransform(transformStr); + expect(xTransform).toEqual(expectedXTransform); + }); - const startTimeMs = 1486656000000; - const interval = 14400000; + test('IE11 String variant.', () => { + const transformStr = 'translate(0.007167499999999999)'; + const xTransform = getXTransform(transformStr); + expect(xTransform).toEqual(expectedXTransform); + }); - const { gAxis, node, vizWidth } = axisSetup({ - interval, - plotEarliest: 1486606500000, - plotLatest: 1486719900000, - startTimeMs, - xAxisTickFormat: 'HH:mm', + test('Invalid String.', () => { + const transformStr = 'translate()'; + const xTransform = getXTransform(transformStr); + expect(xTransform).toEqual(NaN); }); + }); - expect(node.getElementsByTagName('text')).toHaveLength(8); + describe('removeLabelOverlap', () => { + const originalGetBBox = SVGElement.prototype.getBBox; + + // This resembles how ExplorerChart renders its x axis. + // We set up this boilerplate so we can then run removeLabelOverlap() + // on some "real" structure. + function axisSetup({ interval, plotEarliest, plotLatest, startTimeMs, xAxisTickFormat }) { + const { container } = render(
); + const node = container.querySelector('.content-wrapper'); + + const chartHeight = 170; + const margin = { top: 10, right: 0, bottom: 30, left: 60 }; + const svgWidth = 500; + const svgHeight = chartHeight + margin.top + margin.bottom; + const vizWidth = 500; + + const chartElement = d3.select(node); + + const lineChartXScale = d3.time + .scale() + .range([0, vizWidth]) + .domain([plotEarliest, plotLatest]); + + const xAxis = d3.svg + .axis() + .scale(lineChartXScale) + .orient('bottom') + .innerTickSize(-chartHeight) + .outerTickSize(0) + .tickPadding(10) + .tickFormat(d => moment(d).format(xAxisTickFormat)); + + const tickValues = getTickValues(startTimeMs, interval, plotEarliest, plotLatest); + xAxis.tickValues(tickValues); + + const svg = chartElement + .append('svg') + .attr('width', svgWidth) + .attr('height', svgHeight); + + const axes = svg.append('g'); + + const gAxis = axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + chartHeight + ')') + .call(xAxis); - removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); + return { + gAxis, + node, + vizWidth, + }; + } - // at the vizWidth of 500, the most left and right tick label - // will get removed because it overflows the chart area - expect(node.getElementsByTagName('text')).toHaveLength(6); + test('farequote sample data', () => { + const mockedGetBBox = { width: 27.21875 }; + SVGElement.prototype.getBBox = () => mockedGetBBox; - SVGElement.prototype.getBBox = originalGetBBox; - }); + const startTimeMs = 1486656000000; + const interval = 14400000; + + const { gAxis, node, vizWidth } = axisSetup({ + interval, + plotEarliest: 1486606500000, + plotLatest: 1486719900000, + startTimeMs, + xAxisTickFormat: 'HH:mm', + }); - test('filebeat sample data', () => { - const mockedGetBBox = { width: 85.640625 }; - SVGElement.prototype.getBBox = () => mockedGetBBox; + expect(node.getElementsByTagName('text')).toHaveLength(8); - const startTimeMs = 1486080000000; - const interval = 14400000; + removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); - const { gAxis, node, vizWidth } = axisSetup({ - interval, - plotEarliest: 1485860400000, - plotLatest: 1486314000000, - startTimeMs, - xAxisTickFormat: 'YYYY-MM-DD HH:mm', + // at the vizWidth of 500, the most left and right tick label + // will get removed because it overflows the chart area + expect(node.getElementsByTagName('text')).toHaveLength(6); + + SVGElement.prototype.getBBox = originalGetBBox; }); - expect(node.getElementsByTagName('text')).toHaveLength(32); + test('filebeat sample data', () => { + const mockedGetBBox = { width: 85.640625 }; + SVGElement.prototype.getBBox = () => mockedGetBBox; + + const startTimeMs = 1486080000000; + const interval = 14400000; - removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); + const { gAxis, node, vizWidth } = axisSetup({ + interval, + plotEarliest: 1485860400000, + plotLatest: 1486314000000, + startTimeMs, + xAxisTickFormat: 'YYYY-MM-DD HH:mm', + }); - // In this case labels get reduced significantly because of the wider - // labels (full dates + time) and the narrow interval. - expect(node.getElementsByTagName('text')).toHaveLength(3); + expect(node.getElementsByTagName('text')).toHaveLength(32); - SVGElement.prototype.getBBox = originalGetBBox; + removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); + + // In this case labels get reduced significantly because of the wider + // labels (full dates + time) and the narrow interval. + expect(node.getElementsByTagName('text')).toHaveLength(3); + + SVGElement.prototype.getBBox = originalGetBBox; + }); }); }); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.d.ts b/x-pack/plugins/ml/public/application/util/string_utils.d.ts index b5063907e1fdf..531e44e3e78c1 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.d.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.d.ts @@ -14,4 +14,8 @@ export function replaceStringTokens( export function detectorToString(dtr: any): string; +export function sortByKey(list: any, reverse: boolean, comparator?: any): any; + export function toLocaleString(x: number): string; + +export function mlEscape(str: string): string; diff --git a/x-pack/plugins/ml/public/application/util/string_utils.js b/x-pack/plugins/ml/public/application/util/string_utils.js index 172d334099b3d..66835984df5e5 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.js +++ b/x-pack/plugins/ml/public/application/util/string_utils.js @@ -99,211 +99,6 @@ export function sortByKey(list, reverse, comparator) { ); } -// guess the time format for a given time string -export function guessTimeFormat(time) { - let format = ''; - let matched = false; - if (isNaN(time)) { - let match; - - // match date format - if (!matched) { - let reg = ''; - - reg += '('; // 1 ( date - - reg += '('; // 2 ( yyyy-MM-dd - reg += '(\\d{4})'; // 3 yyyy - reg += '([-/.\\s])'; // 4 - or . or \s - reg += '('; // 5 ( month - reg += '([01]\\d)'; // 6 MM - reg += '|'; // or - reg += '(\\w{3})'; // 7 MMM - reg += ')'; // ) end month - reg += '([-/.\\s])'; // 8 - or . or \s - reg += '([0-3]\\d)'; // 9 dd 0-3 and 0-9 - reg += ')'; // ) end yyyy-MM-dd - - reg += '|'; // or - - reg += '('; // 10 ( d[d]-MM[M]-yyyy or MM[M]-d[d]-yyyy - - reg += '('; // 11 ( day or month - reg += '(\\d{1,2})'; // 12 d or M or dd or MM - reg += '|'; // or - reg += '(\\w{3})'; // 13 MMM - reg += ')'; // ) end day or month - - reg += '([-/.\\s])'; // 14 - or . or \s - - reg += '('; // 15 ( day or month - reg += '(\\d{1,2})'; // 12 d or M or dd or MM - reg += '|'; // or - reg += '(\\w{3})'; // 17 MMM - reg += ')'; // ) end day or month - - reg += '([-/.\\s])'; // 18 - or . or \s - reg += '(\\d{4})'; // 19 yyyy - reg += ')'; // ) end d[d]-MM[M]-yyyy or MM[M]-d[d]-yyyy - - reg += ')'; // ) end date - - reg += '([T\\s])?'; // 20 T or space - - reg += '([0-2]\\d)'; // 21 HH 0-2 and 0-9 - reg += '([:.])'; // 22 :. - reg += '([0-5]\\d)'; // 23 mm 0-5 and 0-9 - reg += '('; // 24 ( optional secs - reg += '([:.])'; // 25 :. - reg += '([0-5]\\d)'; // 26 ss 0-5 and 0-9 - reg += ')?'; // ) end optional secs - reg += '('; // 27 ( optional millisecs - reg += '([:.])'; // 28 :. - reg += '(\\d{3})'; // 29 3 * 0-9 - reg += ')?'; // ) end optional millisecs - reg += '('; // 30 ( optional timezone matches - reg += '([+-]\\d{2}[:.]\\d{2}[:.]\\d{2})'; // 31 +- 0-9 0-9 :. 0-9 0-9 :. 0-9 0-9 e.g. +00:00:00 - reg += '|'; // or - reg += '([+-]\\d{2}[:.]\\d{2})'; // 32 +- 0-9 0-9 :. 0-9 0-9 e.g. +00:00 - reg += '|'; // or - reg += '([+-]\\d{6})'; // 33 +- 6 * 0-9 e.g. +000000 - reg += '|'; // or - reg += '([+-]\\d{4})'; // 34 +- 4 * 0-9 e.g. +0000 - reg += '|'; // or - reg += '(Z)'; // 35 Z - reg += '|'; // or - reg += '([+-]\\d{2})'; // 36 +- 0-9 0-9 e.g. +00 - reg += '|'; // or - reg += '('; // 37 ( string timezone - reg += '(\\s)'; // 38 optional space - reg += '(\\w{1,4})'; // 39 1-4 letters e.g UTC - reg += ')'; // ) end string timezone - reg += ')?'; // ) end optional timezone - - console.log('guessTimeFormat: time format regex: ' + reg); - - match = time.match(new RegExp(reg)); - // console.log(match); - if (match) { - // add the standard data and time - if (match[2] !== undefined) { - // match yyyy-[MM MMM]-dd - format += 'yyyy'; - format += match[4]; - if (match[6] !== undefined) { - format += 'MM'; - } else if (match[7] !== undefined) { - format += 'MMM'; - } - format += match[8]; - format += 'dd'; - } else if (match[10] !== undefined) { - // match dd-MM[M]-yyyy or MM[M]-dd-yyyy - - if (match[13] !== undefined) { - // found a word as the first part - // e.g., Jan 01 2000 - format += 'MMM'; - format += match[14]; - format += 'dd'; - } else if (match[17] !== undefined) { - // found a word as the second part - // e.g., 01 Jan 2000 - format += 'dd'; - format += match[14]; - format += 'MMM'; - } else { - // check to see if the first number is greater than 12 - // e.g., 24/03/1981 - // this is a guess, but is only thing we can do - // with one line from the data set - if (match[12] !== undefined && +match[12] > 12) { - format += 'dd'; - format += match[14]; - format += 'MM'; - } else { - // default to US format. - format += 'MM'; - format += match[14]; - format += 'dd'; - } - } - - format += match[18]; - format += 'yyyy'; - } - - // optional T or space splitter - // wrap T in single quotes - format += match[20] === 'T' ? "'" + match[20] + "'" : match[20]; - format += 'HH'; - format += match[22]; - format += 'mm'; - - // add optional secs - if (match[24] !== undefined) { - format += match[25]; - format += 'ss'; - } - - // add optional millisecs - if (match[27] !== undefined) { - // .000 - format += match[28]; - format += 'SSS'; - } - - // add optional time zone - if (match[31] !== undefined) { - // +00:00:00 - format += 'XXXXX'; - } else if (match[32] !== undefined) { - // +00:00 - format += 'XXX'; - } else if (match[33] !== undefined) { - // +000000 - format += 'XXXX'; - } else if (match[34] !== undefined) { - // +0000 - format += 'Z'; - } else if (match[35] !== undefined || match[36] !== undefined) { - // Z or +00 - format += 'X'; - } else if (match[37] !== undefined) { - // UTC - if (match[38] !== undefined) { - // add optional space char - format += match[38]; - } - // add time zone name, up to 4 chars - for (let i = 0; i < match[39].length; i++) { - format += 'z'; - } - } - matched = true; - } - } - } else { - // time field is a number, so probably epoch or epoch_ms - if (time > 10000000000) { - // probably milliseconds - format = 'epoch_ms'; - } else { - // probably seconds - format = 'epoch'; - } - matched = true; - } - - if (matched) { - console.log('guessTimeFormat: guessed time format: ', format); - } else { - console.log('guessTimeFormat: time format could not be guessed from: ' + time); - } - - return format; -} - // add commas to large numbers // Number.toLocaleString is not supported on safari export function toLocaleString(x) { diff --git a/x-pack/plugins/ml/public/application/util/string_utils.test.ts b/x-pack/plugins/ml/public/application/util/string_utils.test.ts new file mode 100644 index 0000000000000..d940fce2ee1d5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/string_utils.test.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + replaceStringTokens, + detectorToString, + sortByKey, + toLocaleString, + mlEscape, + escapeForElasticsearchQuery, +} from './string_utils'; + +describe('ML - string utils', () => { + describe('replaceStringTokens', () => { + const testRecord = { + job_id: 'test_job', + result_type: 'record', + probability: 0.0191711, + record_score: 4.3, + bucket_span: 300, + detector_index: 0, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + user: "Des O'Connor", + testfield1: 'test$tring=[+-?]', + testfield2: '{<()>}', + testfield3: 'host=\\\\test@uk.dev', + }; + + test('returns correct values without URI encoding', () => { + const result = replaceStringTokens('user=$user$,time=$timestamp$', testRecord, false); + expect(result).toBe("user=Des O'Connor,time=1454890500000"); + }); + + test('returns correct values for missing token without URI encoding', () => { + const result = replaceStringTokens('user=$username$,time=$timestamp$', testRecord, false); + expect(result).toBe('user=$username$,time=1454890500000'); + }); + + test('returns correct values with URI encoding', () => { + const testString1 = 'https://www.google.co.uk/webhp#q=$testfield1$'; + const testString2 = 'https://www.google.co.uk/webhp#q=$testfield2$'; + const testString3 = 'https://www.google.co.uk/webhp#q=$testfield3$'; + const testString4 = 'https://www.google.co.uk/webhp#q=$user$'; + + const result1 = replaceStringTokens(testString1, testRecord, true); + const result2 = replaceStringTokens(testString2, testRecord, true); + const result3 = replaceStringTokens(testString3, testRecord, true); + const result4 = replaceStringTokens(testString4, testRecord, true); + + expect(result1).toBe('https://www.google.co.uk/webhp#q=test%24tring%3D%5B%2B-%3F%5D'); + expect(result2).toBe('https://www.google.co.uk/webhp#q=%7B%3C()%3E%7D'); + expect(result3).toBe('https://www.google.co.uk/webhp#q=host%3D%5C%5Ctest%40uk.dev'); + expect(result4).toBe("https://www.google.co.uk/webhp#q=Des%20O'Connor"); + }); + + test('returns correct values for missing token with URI encoding', () => { + const testString = 'https://www.google.co.uk/webhp#q=$username$&time=$timestamp$'; + const result = replaceStringTokens(testString, testRecord, true); + expect(result).toBe('https://www.google.co.uk/webhp#q=$username$&time=1454890500000'); + }); + }); + + describe('detectorToString', () => { + test('returns the correct descriptions for detectors', () => { + const detector1 = { + function: 'count', + }; + + const detector2 = { + function: 'count', + by_field_name: 'airline', + use_null: false, + }; + + const detector3 = { + function: 'mean', + field_name: 'CPUUtilization', + partition_field_name: 'region', + by_field_name: 'host', + over_field_name: 'user', + exclude_frequent: 'all', + }; + + expect(detectorToString(detector1)).toBe('count'); + expect(detectorToString(detector2)).toBe('count by airline use_null=false'); + expect(detectorToString(detector3)).toBe( + 'mean(CPUUtilization) by host over user partition_field_name=region exclude_frequent=all' + ); + }); + }); + + describe('sortByKey', () => { + const obj = { + zebra: 'stripes', + giraffe: 'neck', + elephant: 'trunk', + }; + + const valueComparator = function(value: string) { + return value; + }; + + test('returns correct ordering with default comparator', () => { + const result = sortByKey(obj, false); + const keys = Object.keys(result); + expect(keys[0]).toBe('elephant'); + expect(keys[1]).toBe('giraffe'); + expect(keys[2]).toBe('zebra'); + }); + + test('returns correct ordering with default comparator and order reversed', () => { + const result = sortByKey(obj, true); + const keys = Object.keys(result); + expect(keys[0]).toBe('zebra'); + expect(keys[1]).toBe('giraffe'); + expect(keys[2]).toBe('elephant'); + }); + + test('returns correct ordering with comparator', () => { + const result = sortByKey(obj, false, valueComparator); + const keys = Object.keys(result); + expect(keys[0]).toBe('giraffe'); + expect(keys[1]).toBe('zebra'); + expect(keys[2]).toBe('elephant'); + }); + + test('returns correct ordering with comparator and order reversed', () => { + const result = sortByKey(obj, true, valueComparator); + const keys = Object.keys(result); + expect(keys[0]).toBe('elephant'); + expect(keys[1]).toBe('zebra'); + expect(keys[2]).toBe('giraffe'); + }); + }); + + describe('toLocaleString', () => { + test('returns correct comma placement for large numbers', () => { + expect(toLocaleString(1)).toBe('1'); + expect(toLocaleString(10)).toBe('10'); + expect(toLocaleString(100)).toBe('100'); + expect(toLocaleString(1000)).toBe('1,000'); + expect(toLocaleString(10000)).toBe('10,000'); + expect(toLocaleString(100000)).toBe('100,000'); + expect(toLocaleString(1000000)).toBe('1,000,000'); + expect(toLocaleString(10000000)).toBe('10,000,000'); + expect(toLocaleString(100000000)).toBe('100,000,000'); + expect(toLocaleString(1000000000)).toBe('1,000,000,000'); + }); + }); + + describe('mlEscape', () => { + test('returns correct escaping of characters', () => { + expect(mlEscape('foo&bar')).toBe('foo&bar'); + expect(mlEscape('foobar')).toBe('foo>bar'); + expect(mlEscape('foo"bar')).toBe('foo"bar'); + expect(mlEscape("foo'bar")).toBe('foo'bar'); + expect(mlEscape('foo/bar')).toBe('foo/bar'); + }); + }); + + describe('escapeForElasticsearchQuery', () => { + test('returns correct escaping of reserved elasticsearch characters', () => { + expect(escapeForElasticsearchQuery('foo+bar')).toBe('foo\\+bar'); + expect(escapeForElasticsearchQuery('foo-bar')).toBe('foo\\-bar'); + expect(escapeForElasticsearchQuery('foo=bar')).toBe('foo\\=bar'); + expect(escapeForElasticsearchQuery('foo&&bar')).toBe('foo\\&\\&bar'); + expect(escapeForElasticsearchQuery('foo||bar')).toBe('foo\\|\\|bar'); + expect(escapeForElasticsearchQuery('foo>bar')).toBe('foo\\>bar'); + expect(escapeForElasticsearchQuery('foo, Promise, @@ -298,7 +301,9 @@ export function jobsProvider(callAsCurrentUser: APICaller) { if (calendarResults) { calendarResults.forEach(cal => { cal.job_ids.forEach(id => { - if (groups[id]) { + if (id === GLOBAL_CALENDAR) { + globalCalendars.push(cal.calendar_id); + } else if (groups[id]) { groups[id].forEach(jId => { if (calendarsByJobId[jId] !== undefined) { calendarsByJobId[jId].push(cal.calendar_id); @@ -325,8 +330,12 @@ export function jobsProvider(callAsCurrentUser: APICaller) { jobResults.jobs.forEach(job => { const tempJob = job as CombinedJobWithStats; - if (calendarsByJobId[tempJob.job_id].length) { - tempJob.calendars = calendarsByJobId[tempJob.job_id]; + const calendars: string[] = [ + ...(calendarsByJobId[tempJob.job_id] || []), + ...(globalCalendars || []), + ]; + if (calendars.length) { + tempJob.calendars = calendars; } if (jobStatsResults && jobStatsResults.jobs) { diff --git a/x-pack/plugins/ml/server/routes/README.md b/x-pack/plugins/ml/server/routes/README.md index 1d08335af3d2e..70af73c37dadd 100644 --- a/x-pack/plugins/ml/server/routes/README.md +++ b/x-pack/plugins/ml/server/routes/README.md @@ -6,11 +6,14 @@ Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotatio to generate documentation. The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. -For now the process is pretty manual. You need to make sure the packages mentioned above are installed globally -to execute the following command from the directory in which this README file is located. +There are custom parser and worker (`x-pack/plugins/ml/server/routes/apidoc_scripts`) to process api schemas for each documentation entry. It's written with typescript so make sure all the scripts in the folder are compiled before executing `apidoc` command. + +Make sure you have run `yarn kbn bootstrap` to get all requires dev dependencies. Then execute the following command from the ml plugin folder: ``` -apidoc -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md +yarn run apiDocs ``` +It compiles all the required scripts and generates the documentation both in HTML and Markdown formats. + It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format -as well as `ML_API.md` file. \ No newline at end of file +as well as `ML_API.md` file. diff --git a/x-pack/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts index 56c0b639e2c85..d5abebda00caa 100644 --- a/x-pack/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -5,10 +5,8 @@ */ import Boom from 'boom'; -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; import { SecurityPluginSetup } from '../../../security/server'; import { isAnnotationsFeatureAvailable } from '../lib/check_annotations'; import { annotationServiceProvider } from '../models/annotation_service'; @@ -45,10 +43,7 @@ export function annotationRoutes( * @apiName GetAnnotations * @apiDescription Gets annotations. * - * @apiParam {String[]} jobIds List of job IDs - * @apiParam {String} earliestMs - * @apiParam {Number} latestMs - * @apiParam {Number} maxAnnotations Max limit of annotations returned + * @apiSchema (body) getAnnotationsSchema * * @apiSuccess {Boolean} success * @apiSuccess {Object} annotations @@ -57,7 +52,7 @@ export function annotationRoutes( { path: '/api/ml/annotations', validate: { - body: schema.object(getAnnotationsSchema), + body: getAnnotationsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -83,14 +78,13 @@ export function annotationRoutes( * @apiName IndexAnnotations * @apiDescription Index the annotation. * - * @apiParam {Object} annotation - * @apiParam {String} username + * @apiSchema (body) indexAnnotationSchema */ router.put( { path: '/api/ml/annotations/index', validate: { - body: schema.object(indexAnnotationSchema), + body: indexAnnotationSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -124,17 +118,17 @@ export function annotationRoutes( /** * @apiGroup Annotations * - * @api {delete} /api/ml/annotations/index Deletes annotation + * @api {delete} /api/ml/annotations/delete/:annotationId Deletes annotation * @apiName DeleteAnnotation * @apiDescription Deletes specified annotation * - * @apiParam {String} annotationId + * @apiSchema (params) deleteAnnotationSchema */ router.delete( { path: '/api/ml/annotations/delete/{annotationId}', validate: { - params: schema.object(deleteAnnotationSchema), + params: deleteAnnotationSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index d03e76072c315..a675eb58dc792 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -10,6 +10,13 @@ import { RouteInitialization } from '../types'; import { anomalyDetectionJobSchema, anomalyDetectionUpdateJobSchema, + jobIdSchema, + getRecordsSchema, + getBucketsSchema, + getOverallBucketsSchema, + getCategoriesSchema, + forecastAnomalyDetector, + getBucketParamsSchema, } from './schemas/anomaly_detectors_schema'; /** @@ -50,15 +57,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetAnomalyDetectorsById * @apiDescription Returns the anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.get( { path: '/api/ml/anomaly_detectors/{jobId}', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -108,15 +113,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetAnomalyDetectorsStatsById * @apiDescription Returns anomaly detection job statistics. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.get( { path: '/api/ml/anomaly_detectors/{jobId}/_stats', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -139,15 +142,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName CreateAnomalyDetectors * @apiDescription Creates an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) anomalyDetectionJobSchema */ router.put( { path: '/api/ml/anomaly_detectors/{jobId}', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, body: schema.object(anomalyDetectionJobSchema), }, }, @@ -174,16 +176,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName UpdateAnomalyDetectors * @apiDescription Updates certain properties of an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) anomalyDetectionUpdateJobSchema */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_update', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ ...anomalyDetectionUpdateJobSchema }), + params: jobIdSchema, + body: anomalyDetectionUpdateJobSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -209,15 +210,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName OpenAnomalyDetectorsJob * @apiDescription Opens an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_open', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -242,15 +241,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName CloseAnomalyDetectorsJob * @apiDescription Closes an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_close', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -279,15 +276,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName DeleteAnomalyDetectorsJob * @apiDescription Deletes specified anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.delete( { path: '/api/ml/anomaly_detectors/{jobId}', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -315,8 +310,6 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/anomaly_detectors/_validate/detector Validate detector * @apiName ValidateAnomalyDetector * @apiDescription Validates specified detector. - * - * @apiParam {String} jobId Job ID. */ router.post( { @@ -346,16 +339,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName ForecastAnomalyDetector * @apiDescription Creates a forecast for the specified anomaly detection job, predicting the future behavior of a time series by using its historical behavior. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) forecastAnomalyDetector */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_forecast', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ duration: schema.any() }), + params: jobIdSchema, + body: forecastAnomalyDetector, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -382,7 +374,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetRecords * @apiDescription Retrieves anomaly records for a job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) getRecordsSchema * * @apiSuccess {Number} count * @apiSuccess {Object[]} records @@ -391,23 +384,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/{jobId}/results/records', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ - desc: schema.maybe(schema.boolean()), - end: schema.maybe(schema.string()), - exclude_interim: schema.maybe(schema.boolean()), - page: schema.maybe( - schema.object({ - from: schema.maybe(schema.number()), - size: schema.maybe(schema.number()), - }) - ), - record_score: schema.maybe(schema.number()), - sort: schema.maybe(schema.string()), - start: schema.maybe(schema.string()), - }), + params: jobIdSchema, + body: getRecordsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -432,8 +410,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetBuckets * @apiDescription The get buckets API presents a chronological view of the records, grouped by bucket. * - * @apiParam {String} jobId Job ID. - * @apiParam {String} timestamp. + * @apiSchema (params) getBucketParamsSchema + * @apiSchema (body) getBucketsSchema * * @apiSuccess {Number} count * @apiSuccess {Object[]} buckets @@ -442,25 +420,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/{jobId}/results/buckets/{timestamp?}', validate: { - params: schema.object({ - jobId: schema.string(), - timestamp: schema.maybe(schema.string()), - }), - body: schema.object({ - anomaly_score: schema.maybe(schema.number()), - desc: schema.maybe(schema.boolean()), - end: schema.maybe(schema.string()), - exclude_interim: schema.maybe(schema.boolean()), - expand: schema.maybe(schema.boolean()), - page: schema.maybe( - schema.object({ - from: schema.maybe(schema.number()), - size: schema.maybe(schema.number()), - }) - ), - sort: schema.maybe(schema.string()), - start: schema.maybe(schema.string()), - }), + params: getBucketParamsSchema, + body: getBucketsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -486,7 +447,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetOverallBuckets * @apiDescription Retrieves overall bucket results that summarize the bucket results of multiple anomaly detection jobs. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) getOverallBucketsSchema * * @apiSuccess {Number} count * @apiSuccess {Object[]} overall_buckets @@ -495,15 +457,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/{jobId}/results/overall_buckets', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ - topN: schema.number(), - bucketSpan: schema.string(), - start: schema.number(), - end: schema.number(), - }), + params: jobIdSchema, + body: getOverallBucketsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -531,17 +486,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetCategories * @apiDescription Returns the categories results for the specified job ID and category ID. * - * @apiParam {String} jobId Job ID. - * @apiParam {String} categoryId Category ID. + * @apiSchema (params) getCategoriesSchema */ router.get( { path: '/api/ml/anomaly_detectors/{jobId}/results/categories/{categoryId}', validate: { - params: schema.object({ - categoryId: schema.string(), - jobId: schema.string(), - }), + params: getCategoriesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index c5aa3e4d792fd..4848de6db7049 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -1,6 +1,6 @@ { "name": "ml_kibana_api", - "version": "0.1.0", + "version": "7.8.0", "description": "ML Kibana API", "title": "ML Kibana API", "order": [ @@ -9,61 +9,65 @@ "GetDataFrameAnalyticsById", "GetDataFrameAnalyticsStats", "GetDataFrameAnalyticsStatsById", - "UpdateDataFrameAnalytics", "EvaluateDataFrameAnalytics", "ExplainDataFrameAnalytics", - "DeleteDataFrameAnalytics", "StartDataFrameAnalyticsJob", "StopsDataFrameAnalyticsJob", "GetDataFrameAnalyticsMessages", + "UpdateDataFrameAnalytics", + "DeleteDataFrameAnalytics", + "DataVisualizer", "GetOverallStats", "GetStatsForFields", + "AnomalyDetectors", + "CreateAnomalyDetectors", + "OpenAnomalyDetectorsJob", "GetAnomalyDetectors", "GetAnomalyDetectorsById", "GetAnomalyDetectorsStats", "GetAnomalyDetectorsStatsById", - "CreateAnomalyDetectors", - "UpdateAnomalyDetectors", - "OpenAnomalyDetectorsJob", "CloseAnomalyDetectorsJob", - "DeleteAnomalyDetectorsJob", "ValidateAnomalyDetector", "ForecastAnomalyDetector", "GetRecords", "GetBuckets", "GetOverallBuckets", "GetCategories", + "UpdateAnomalyDetectors", + "DeleteAnomalyDetectorsJob", + "FileDataVisualizer", "AnalyzeFile", "ImportFile", + "ResultsService", "GetAnomaliesTableData", "GetCategoryDefinition", "GetMaxAnomalyScore", "GetCategoryExamples", "GetPartitionFieldsValues", + "DataRecognizer", "RecognizeIndex", "GetModule", "SetupModule", "CheckExistingModuleJobs", + "Annotations", "GetAnnotations", "IndexAnnotations", "DeleteAnnotation", + "JobService", "ForceStartDatafeeds", "StopDatafeeds", - "DeleteJobs", "CloseJobs", "JobsSummary", "JobsWithTimeRange", "CreateFullJobsList", "GetAllGroups", - "UpdateGroups", - "DeletingJobTasks", "JobsExist", "NewJobCaps", "NewJobLineChart", @@ -72,42 +76,60 @@ "GetLookBackProgress", "ValidateCategoryExamples", "TopCategories", + "UpdateGroups", + "DeletingJobTasks", + "DeleteJobs", + + "Calendars", + "PutCalendars", + "GetCalendars", + "GetCalendarById", + "UpdateCalendarById", + "DeleteCalendarById", + "Filters", + "CreateFilter", "GetFilters", "GetFilterById", - "CreateFilter", + "GetFiltersStats", "UpdateFilter", "DeleteFilter", - "GetFiltersStats", + "Indices", "FieldCaps", + "SystemRoutes", "HasPrivileges", "MlCapabilities", "MlNodeCount", "MlInfo", "MlEsSearch", + "JobAuditMessages", "GetJobAuditMessages", "GetAllJobAuditMessages", + "JobValidation", "EstimateBucketSpan", "CalculateModelMemoryLimit", "ValidateCardinality", "ValidateJob", + "NotificationSettings", "GetNotificationSettings", + "DatafeedService", + "CreateDatafeed", + "PreviewDatafeed", "GetDatafeeds", "GetDatafeed", "GetDatafeedsStats", "GetDatafeedStats", - "CreateDatafeed", "UpdateDatafeed", - "DeleteDatafeed", "StartDatafeed", "StopDatafeed", - "PreviewDatafeed", + "DeleteDatafeed", + "FieldsService", "GetCardinalityOfFields", "GetTimeFieldRange" diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.ts new file mode 100644 index 0000000000000..01adcb462689e --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as ts from 'typescript'; + +export interface DocEntry { + name: string; + documentation?: string; + type: string; + optional?: boolean; + nested?: DocEntry[]; +} + +/** Generate documentation for all schema definitions in a set of .ts files */ +export function extractDocumentation( + fileNames: string[], + options: ts.CompilerOptions = { + target: ts.ScriptTarget.ES2015, + module: ts.ModuleKind.CommonJS, + } +): Map { + // Build a program using the set of root file names in fileNames + const program = ts.createProgram(fileNames, options); + + // Get the checker, we will use it to find more about properties + const checker: ts.TypeChecker = program.getTypeChecker(); + + // Result map + const result = new Map(); + + // Visit every sourceFile in the program + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + // Walk the tree to search for schemas + ts.forEachChild(sourceFile, visit); + } + } + + return result; + + /** visit nodes finding exported schemas */ + function visit(node: ts.Node) { + if (isNodeExported(node) && ts.isVariableDeclaration(node)) { + const schemaName = node.name.getText(); + const schemaType = checker.getTypeAtLocation(node); + result.set(schemaName, extractDocEntries(schemaType!)); + } + + if (node.getChildCount() > 0) { + ts.forEachChild(node, visit); + } + } + + /** + * Extracts doc entries for the schema definition + * @param schemaType + */ + function extractDocEntries(schemaType: ts.Type): DocEntry[] { + const collection: DocEntry[] = []; + + const members = getTypeMembers(schemaType); + + if (!members) { + return collection; + } + + members.forEach(member => { + collection.push(serializeProperty(member)); + }); + + return collection; + } + + /** + * Resolves members of the type + * @param type + */ + function getTypeMembers(type: ts.Type): ts.Symbol[] | undefined { + const argsOfType = checker.getTypeArguments((type as unknown) as ts.TypeReference); + + let members = type.getProperties(); + + if (argsOfType && argsOfType.length > 0) { + members = argsOfType[0].getProperties(); + } + + return members; + } + + function resolveTypeArgument(type: ts.Type): ts.SymbolTable | string { + // required to extract members + type.getProperty('type'); + + // @ts-ignores + let members = type.members; + + const typeArguments = checker.getTypeArguments((type as unknown) as ts.TypeReference); + + if (type.aliasTypeArguments) { + // @ts-ignores + members = type.aliasTypeArguments[0].members; + } + + if (typeArguments.length > 0) { + members = resolveTypeArgument(typeArguments[0]); + } + + if (members === undefined) { + members = checker.typeToString(type); + } + + return members; + } + + function serializeProperty(symbol: ts.Symbol): DocEntry { + // @ts-ignore + const typeOfSymbol = symbol.type; + const typeArguments = checker.getTypeArguments((typeOfSymbol as unknown) as ts.TypeReference); + + let resultType: ts.Type = typeOfSymbol; + + let members; + if (typeArguments.length > 0) { + members = resolveTypeArgument(typeArguments[0]); + resultType = typeArguments[0]; + } + + let typeAsString = checker.typeToString(resultType); + + const nestedEntries: DocEntry[] = []; + if (members && typeof members !== 'string' && members.size > 0) { + // we hit an object or collection + typeAsString = + resultType.symbol.name === 'Array' || typeOfSymbol.symbol.name === 'Array' + ? `${symbol.getName()}[]` + : symbol.getName(); + + members.forEach(member => { + nestedEntries.push(serializeProperty(member)); + }); + } + + return { + name: symbol.getName(), + documentation: getCommentString(symbol), + type: typeAsString, + ...(nestedEntries.length > 0 ? { nested: nestedEntries } : {}), + }; + } + + function getCommentString(symbol: ts.Symbol): string { + return ts.displayPartsToString(symbol.getDocumentationComment(checker)).replace(/\n/g, ' '); + } + + /** + * True if this is visible outside this file, false otherwise + */ + function isNodeExported(node: ts.Node): boolean { + return ( + // eslint-disable-next-line no-bitwise + (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 || + (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) + ); + } +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_parser.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_parser.ts new file mode 100644 index 0000000000000..eabe7dcd7bd8f --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_parser.ts @@ -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. + */ + +function parse(content?: string) { + const schema = typeof content === 'string' && content.trim(); + + if (!schema) { + return null; + } + + const result = schema.match(/\((\w+)\)\s+(\w+)/); + + if (result === null || result.length < 3) { + throw new Error( + 'Invalid schema definition. Required format is `@apiSchema () `' + ); + } + + const group = result[1]; + + return { + group, + name: result[2], + }; +} + +/** + * Exports + */ +module.exports = { + parse, + path: 'local.schemas', + method: 'push', +}; diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_worker.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_worker.ts new file mode 100644 index 0000000000000..7514e482783b3 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_worker.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 * as fs from 'fs'; +import * as path from 'path'; +import { DocEntry, extractDocumentation } from './schema_extractor'; +import { ApiParameter, Block } from './types'; + +export function postProcess(parsedFiles: any[]): void { + const schemasDirPath = `${__dirname}${path.sep}..${path.sep}..${path.sep}schemas${path.sep}`; + const schemaFiles = fs + .readdirSync(schemasDirPath) + .map(filename => path.resolve(schemasDirPath + filename)); + + const schemaDocs = extractDocumentation(schemaFiles); + + parsedFiles.forEach(parsedFile => { + parsedFile.forEach((block: Block) => { + const { + local: { schemas }, + } = block; + if (!schemas || schemas.length === 0) return; + + for (const schema of schemas) { + const { name: schemaName, group: paramsGroup } = schema; + const schemaFields = schemaDocs.get(schemaName); + + if (!schemaFields) return; + + updateBlockParameters(schemaFields, block, paramsGroup); + } + }); + }); +} + +/** + * Extracts schema's doc entries to apidoc parameters + * @param docEntries + * @param block + * @param paramsGroup + */ +function updateBlockParameters(docEntries: DocEntry[], block: Block, paramsGroup: string): void { + if (!block.local.parameter) { + block.local.parameter = { + fields: {}, + }; + } + + if (!block.local.parameter.fields![paramsGroup]) { + block.local.parameter.fields![paramsGroup] = []; + } + const collection = block.local.parameter.fields![paramsGroup] as ApiParameter[]; + + for (const field of docEntries) { + collection.push({ + group: paramsGroup, + type: escapeSpecial(field.type), + size: undefined, + allowedValues: undefined, + optional: !!field.optional, + field: field.name, + defaultValue: undefined, + description: field.documentation, + }); + + if (field.nested) { + updateBlockParameters(field.nested, block, field.name); + } + } +} + +/** + * Escape special character to make sure the markdown table isn't broken + */ +function escapeSpecial(str: string): string { + return str.replace(/\|/g, '\\|'); +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json new file mode 100644 index 0000000000000..e3108b8c759f4 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "target": "es6", + "moduleResolution": "node" + }, + "include": [ + "schema_worker.ts", + "schema_parser.ts", + "schema_extractor.ts", + "version_filter.ts" + ] +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/types.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/types.ts new file mode 100644 index 0000000000000..08a443905ee05 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ApiParameter { + group: string; + type: any; + size: undefined; + allowedValues: undefined; + optional: boolean; + field: string; + defaultValue: undefined; + description?: string; +} + +interface Local { + group: string; + type: string; + url: string; + title: string; + name: string; + description: string; + parameter: { + fields?: { + [key: string]: ApiParameter[] | undefined; + }; + }; + success: { fields: ObjectConstructor[] }; + version: string; + filename: string; + schemas?: Array<{ + name: string; + group: string; + }>; +} + +export interface Block { + global: any; + local: Local; +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts new file mode 100644 index 0000000000000..8cbe38d667b2c --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.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 { Block } from './types'; + +const API_VERSION = '7.8.0'; + +/** + * Post Filter parsed results. + * Updates api version of the endpoints. + */ +export function postFilter(parsedFiles: any[]) { + parsedFiles.forEach(parsedFile => { + parsedFile.forEach((block: Block) => { + block.local.version = API_VERSION; + }); + }); +} diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index 34950c6ed79f7..a17601f74ae93 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -5,10 +5,9 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; -import { calendarSchema } from './schemas/calendars_schema'; +import { calendarSchema, calendarIdSchema, calendarIdsSchema } from './schemas/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { @@ -42,7 +41,13 @@ function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) } export function calendars({ router, mlLicense }: RouteInitialization) { - // Gets calendars - size limit has been explicitly set to 1000 + /** + * @apiGroup Calendars + * + * @api {get} /api/ml/calendars Gets calendars + * @apiName GetCalendars + * @apiDescription Gets calendars - size limit has been explicitly set to 1000 + */ router.get( { path: '/api/ml/calendars', @@ -61,11 +66,20 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {get} /api/ml/calendars/:calendarIds Gets a calendar + * @apiName GetCalendarById + * @apiDescription Gets calendar by id + * + * @apiSchema (params) calendarIdsSchema + */ router.get( { path: '/api/ml/calendars/{calendarIds}', validate: { - params: schema.object({ calendarIds: schema.string() }), + params: calendarIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -88,11 +102,20 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {put} /api/ml/calendars Creates a calendar + * @apiName PutCalendars + * @apiDescription Creates a calendar + * + * @apiSchema (body) calendarSchema + */ router.put( { path: '/api/ml/calendars', validate: { - body: schema.object({ ...calendarSchema }), + body: calendarSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -109,12 +132,22 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {put} /api/ml/calendars/:calendarId Updates a calendar + * @apiName UpdateCalendarById + * @apiDescription Updates a calendar + * + * @apiSchema (params) calendarIdSchema + * @apiSchema (body) calendarSchema + */ router.put( { path: '/api/ml/calendars/{calendarId}', validate: { - params: schema.object({ calendarId: schema.string() }), - body: schema.object({ ...calendarSchema }), + params: calendarIdSchema, + body: calendarSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -132,11 +165,20 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {delete} /api/ml/calendars/:calendarId Deletes a calendar + * @apiName DeleteCalendarById + * @apiDescription Deletes a calendar + * + * @apiSchema (params) calendarIdSchema + */ router.delete( { path: '/api/ml/calendars/{calendarId}', validate: { - params: schema.object({ calendarId: schema.string() }), + params: calendarIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 7ed1aa02b24ab..dd9e0ea66aa9d 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; import { RouteInitialization } from '../types'; @@ -12,6 +11,8 @@ import { dataAnalyticsJobConfigSchema, dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, + analyticsIdSchema, + stopsDataFrameAnalyticsJobQuerySchema, } from './schemas/data_analytics_schema'; /** @@ -31,9 +32,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat router.get( { path: '/api/ml/data_frame/analytics', - validate: { - params: schema.object({ analyticsId: schema.maybe(schema.string()) }), - }, + validate: false, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -54,13 +53,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName GetDataFrameAnalyticsById * @apiDescription Returns the data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { - params: schema.object({ analyticsId: schema.string() }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -111,13 +110,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName GetDataFrameAnalyticsStatsById * @apiDescription Returns data frame analytics job statistics. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', validate: { - params: schema.object({ analyticsId: schema.string() }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -146,16 +145,15 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiDescription This API creates a data frame analytics job that performs an analysis * on the source index and stores the outcome in a destination index. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema + * @apiSchema (body) dataAnalyticsJobConfigSchema */ router.put( { path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { - params: schema.object({ - analyticsId: schema.string(), - }), - body: schema.object(dataAnalyticsJobConfigSchema), + params: analyticsIdSchema, + body: dataAnalyticsJobConfigSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -183,12 +181,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @api {post} /api/ml/data_frame/_evaluate Evaluate the data frame analytics for an annotated index * @apiName EvaluateDataFrameAnalytics * @apiDescription Evaluates the data frame analytics for an annotated index. + * + * @apiSchema (body) dataAnalyticsEvaluateSchema */ router.post( { path: '/api/ml/data_frame/_evaluate', validate: { - body: schema.object({ ...dataAnalyticsEvaluateSchema }), + body: dataAnalyticsEvaluateSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -216,19 +216,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiDescription This API provides explanations for a data frame analytics config * that either exists already or one that has not been created yet. * - * @apiParam {String} [description] - * @apiParam {Object} [dest] - * @apiParam {Object} source - * @apiParam {String} source.index - * @apiParam {Object} analysis - * @apiParam {Object} [analyzed_fields] - * @apiParam {String} [model_memory_limit] + * @apiSchema (body) dataAnalyticsExplainSchema */ router.post( { path: '/api/ml/data_frame/analytics/_explain', validate: { - body: schema.object({ ...dataAnalyticsExplainSchema }), + body: dataAnalyticsExplainSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -255,15 +249,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName DeleteDataFrameAnalytics * @apiDescription Deletes specified data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.delete( { path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { - params: schema.object({ - analyticsId: schema.string(), - }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -291,15 +283,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName StartDataFrameAnalyticsJob * @apiDescription Starts a data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_start', validate: { - params: schema.object({ - analyticsId: schema.string(), - }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -324,16 +314,15 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName StopsDataFrameAnalyticsJob * @apiDescription Stops a data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema + * @apiSchema (query) stopsDataFrameAnalyticsJobQuerySchema */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', validate: { - params: schema.object({ - analyticsId: schema.string(), - force: schema.maybe(schema.boolean()), - }), + params: analyticsIdSchema, + query: stopsDataFrameAnalyticsJobQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -367,13 +356,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName GetDataFrameAnalyticsMessages * @apiDescription Returns the list of audit messages for data frame analytics jobs. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/messages', validate: { - params: schema.object({ analyticsId: schema.string() }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index b37c80b815e1a..a4c0d5553a4b2 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -11,6 +11,7 @@ import { Field } from '../models/data_visualizer/data_visualizer'; import { dataVisualizerFieldStatsSchema, dataVisualizerOverallStatsSchema, + indexPatternTitleSchema, } from './schemas/data_visualizer_schema'; import { RouteInitialization } from '../types'; @@ -75,12 +76,16 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * @apiName GetStatsForFields * @apiDescription Returns fields stats of the index pattern. * - * @apiParam {String} indexPatternTitle Index pattern title. + * @apiSchema (params) indexPatternTitleSchema + * @apiSchema (body) dataVisualizerFieldStatsSchema */ router.post( { path: '/api/ml/data_visualizer/get_field_stats/{indexPatternTitle}', - validate: dataVisualizerFieldStatsSchema, + validate: { + params: indexPatternTitleSchema, + body: dataVisualizerFieldStatsSchema, + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -127,12 +132,16 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * @apiName GetOverallStats * @apiDescription Returns overall stats of the index pattern. * - * @apiParam {String} indexPatternTitle Index pattern title. + * @apiSchema (params) indexPatternTitleSchema + * @apiSchema (body) dataVisualizerOverallStatsSchema */ router.post( { path: '/api/ml/data_visualizer/get_overall_stats/{indexPatternTitle}', - validate: dataVisualizerOverallStatsSchema, + validate: { + params: indexPatternTitleSchema, + body: dataVisualizerOverallStatsSchema, + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index c1ee839340996..ec667e1d305f5 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; -import { startDatafeedSchema, datafeedConfigSchema } from './schemas/datafeeds_schema'; +import { + startDatafeedSchema, + datafeedConfigSchema, + datafeedIdSchema, + deleteDatafeedQuerySchema, +} from './schemas/datafeeds_schema'; /** * Routes for datafeed service @@ -44,12 +48,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {get} /api/ml/datafeeds/:datafeedId Get datafeed for given datafeed id * @apiName GetDatafeed * @apiDescription Retrieves configuration information for datafeed + * + * @apiSchema (params) datafeedIdSchema */ router.get( { path: '/api/ml/datafeeds/{datafeedId}', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -97,12 +103,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {get} /api/ml/datafeeds/:datafeedId/_stats Get datafeed stats for given datafeed id * @apiName GetDatafeedStats * @apiDescription Retrieves usage information for datafeed + * + * @apiSchema (params) datafeedIdSchema */ router.get( { path: '/api/ml/datafeeds/{datafeedId}/_stats', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -127,12 +135,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {put} /api/ml/datafeeds/:datafeedId Creates datafeed * @apiName CreateDatafeed * @apiDescription Instantiates a datafeed + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (body) datafeedConfigSchema */ router.put( { path: '/api/ml/datafeeds/{datafeedId}', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, body: datafeedConfigSchema, }, }, @@ -159,12 +170,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/datafeeds/:datafeedId/_update Updates datafeed for given datafeed id * @apiName UpdateDatafeed * @apiDescription Updates certain properties of a datafeed + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (body) datafeedConfigSchema */ router.post( { path: '/api/ml/datafeeds/{datafeedId}/_update', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, body: datafeedConfigSchema, }, }, @@ -191,13 +205,16 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {delete} /api/ml/datafeeds/:datafeedId Deletes datafeed * @apiName DeleteDatafeed * @apiDescription Deletes an existing datafeed + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (query) deleteDatafeedQuerySchema */ router.delete( { path: '/api/ml/datafeeds/{datafeedId}', validate: { - params: schema.object({ datafeedId: schema.string() }), - query: schema.maybe(schema.object({ force: schema.maybe(schema.any()) })), + params: datafeedIdSchema, + query: deleteDatafeedQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -227,12 +244,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/datafeeds/:datafeedId/_start Starts datafeed for given datafeed id(s) * @apiName StartDatafeed * @apiDescription Starts one or more datafeeds + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (body) startDatafeedSchema */ router.post( { path: '/api/ml/datafeeds/{datafeedId}/_start', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, body: startDatafeedSchema, }, }, @@ -262,12 +282,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/datafeeds/:datafeedId/_stop Stops datafeed for given datafeed id(s) * @apiName StopDatafeed * @apiDescription Stops one or more datafeeds + * + * @apiSchema (params) datafeedIdSchema */ router.post( { path: '/api/ml/datafeeds/{datafeedId}/_stop', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -293,12 +315,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {get} /api/ml/datafeeds/:datafeedId/_preview Preview datafeed for given datafeed id * @apiName PreviewDatafeed * @apiDescription Previews a datafeed + * + * @apiSchema (params) datafeedIdSchema */ router.get( { path: '/api/ml/datafeeds/{datafeedId}/_preview', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index db7613b163457..9a5f47409c8a0 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -35,6 +35,8 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/fields_service/field_cardinality Get cardinality of fields * @apiName GetCardinalityOfFields * @apiDescription Returns the cardinality of one or more fields. Returns an Object whose keys are the names of the fields, with values equal to the cardinality of the field + * + * @apiSchema (body) getCardinalityOfFieldsSchema */ router.post( { @@ -63,6 +65,8 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/fields_service/time_field_range Get time field range * @apiName GetTimeFieldRange * @apiDescription Returns the timefield range for the given index + * + * @apiSchema (body) getTimeFieldRangeSchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index b915d13aa9720..3f3fc3f547b6a 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; -import { MAX_BYTES } from '../../common/constants/file_datavisualizer'; +import { MAX_FILE_SIZE_BYTES } from '../../common/constants/file_datavisualizer'; import { InputOverrides, Settings, @@ -22,6 +22,11 @@ import { import { RouteInitialization } from '../types'; import { updateTelemetry } from '../lib/telemetry'; +import { + analyzeFileQuerySchema, + importFileBodySchema, + importFileQuerySchema, +} from './schemas/file_data_visualizer_schema'; function analyzeFiles(context: RequestHandlerContext, data: InputData, overrides: InputOverrides) { const { analyzeFile } = fileDataVisualizerProvider(context.ml!.mlClient.callAsCurrentUser); @@ -51,35 +56,20 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat * @api {post} /api/ml/file_data_visualizer/analyze_file Analyze file data * @apiName AnalyzeFile * @apiDescription Performs analysis of the file data. + * + * @apiSchema (query) analyzeFileQuerySchema */ router.post( { path: '/api/ml/file_data_visualizer/analyze_file', validate: { body: schema.any(), - query: schema.maybe( - schema.object({ - charset: schema.maybe(schema.string()), - column_names: schema.maybe(schema.string()), - delimiter: schema.maybe(schema.string()), - explain: schema.maybe(schema.string()), - format: schema.maybe(schema.string()), - grok_pattern: schema.maybe(schema.string()), - has_header_row: schema.maybe(schema.string()), - line_merge_size_limit: schema.maybe(schema.string()), - lines_to_sample: schema.maybe(schema.string()), - quote: schema.maybe(schema.string()), - should_trim_fields: schema.maybe(schema.string()), - timeout: schema.maybe(schema.string()), - timestamp_field: schema.maybe(schema.string()), - timestamp_format: schema.maybe(schema.string()), - }) - ), + query: analyzeFileQuerySchema, }, options: { body: { accepts: ['text/*', 'application/json'], - maxBytes: MAX_BYTES, + maxBytes: MAX_FILE_SIZE_BYTES, }, }, }, @@ -99,29 +89,21 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat * @api {post} /api/ml/file_data_visualizer/import Import file data * @apiName ImportFile * @apiDescription Imports file data into elasticsearch index. + * + * @apiSchema (query) importFileQuerySchema + * @apiSchema (body) importFileBodySchema */ router.post( { path: '/api/ml/file_data_visualizer/import', validate: { - query: schema.object({ - id: schema.maybe(schema.string()), - }), - body: schema.object({ - index: schema.maybe(schema.string()), - data: schema.arrayOf(schema.any()), - settings: schema.maybe(schema.any()), - mappings: schema.any(), - ingestPipeline: schema.object({ - id: schema.maybe(schema.string()), - pipeline: schema.maybe(schema.any()), - }), - }), + query: importFileQuerySchema, + body: importFileBodySchema, }, options: { body: { accepts: ['application/json'], - maxBytes: MAX_BYTES, + maxBytes: MAX_FILE_SIZE_BYTES, }, }, }, diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index e827ed96b12af..738c25070358d 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -5,10 +5,9 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; -import { createFilterSchema, updateFilterSchema } from './schemas/filters_schema'; +import { createFilterSchema, filterIdSchema, updateFilterSchema } from './schemas/filters_schema'; import { FilterManager, FormFilter } from '../models/filter'; // TODO - add function for returning a list of just the filter IDs. @@ -79,6 +78,8 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetFilterById * @apiDescription Retrieves the filter with the specified ID. * + * @apiSchema (params) filterIdSchema + * * @apiSuccess {Boolean} success * @apiSuccess {Object} filter the filter with the specified ID */ @@ -86,7 +87,7 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/{filterId}', validate: { - params: schema.object({ filterId: schema.string() }), + params: filterIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -108,6 +109,8 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName CreateFilter * @apiDescription Instantiates a filter, for use by custom rules in anomaly detection. * + * @apiSchema (body) createFilterSchema + * * @apiSuccess {Boolean} success * @apiSuccess {Object} filter created filter */ @@ -115,7 +118,7 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters', validate: { - body: schema.object(createFilterSchema), + body: createFilterSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -139,6 +142,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName UpdateFilter * @apiDescription Updates the description of a filter, adds items or removes items. * + * @apiSchema (params) filterIdSchema + * @apiSchema (body) updateFilterSchema + * * @apiSuccess {Boolean} success * @apiSuccess {Object} filter updated filter */ @@ -146,8 +152,8 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/{filterId}', validate: { - params: schema.object({ filterId: schema.string() }), - body: schema.object(updateFilterSchema), + params: filterIdSchema, + body: updateFilterSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -172,13 +178,13 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName DeleteFilter * @apiDescription Deletes the filter with the specified ID. * - * @apiParam {String} filterId the ID of the filter to delete + * @apiSchema (params) filterIdSchema */ router.delete( { path: '/api/ml/filters/{filterId}', validate: { - params: schema.object({ filterId: schema.string() }), + params: filterIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index fe66cc8b01396..e434936beba63 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; +import { indicesSchema } from './schemas/indices_schema'; /** * Indices routes. @@ -15,18 +15,17 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup Indices * - * @api {post} /api/ml/indices/field_caps + * @api {post} /api/ml/indices/field_caps Field caps * @apiName FieldCaps * @apiDescription Retrieves the capabilities of fields among multiple indices. + * + * @apiSchema (body) indicesSchema */ router.post( { path: '/api/ml/indices/field_caps', validate: { - body: schema.object({ - index: schema.maybe(schema.string()), - fields: schema.maybe(schema.arrayOf(schema.string())), - }), + body: indicesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 5c6d8023cc172..71499748691f6 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; +import { jobAuditMessagesQuerySchema, jobIdSchema } from './schemas/job_audit_messages_schema'; /** * Routes for job audit message routes @@ -19,13 +19,16 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio * @api {get} /api/ml/job_audit_messages/messages/:jobId Get audit messages * @apiName GetJobAuditMessages * @apiDescription Returns audit messages for specified job ID + * + * @apiSchema (params) jobIdSchema + * @apiSchema (query) jobAuditMessagesQuerySchema */ router.get( { path: '/api/ml/job_audit_messages/messages/{jobId}', validate: { - params: schema.object({ jobId: schema.maybe(schema.string()) }), - query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), + params: jobIdSchema, + query: jobAuditMessagesQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -52,13 +55,14 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio * @api {get} /api/ml/job_audit_messages/messages Get all audit messages * @apiName GetAllJobAuditMessages * @apiDescription Returns all audit messages + * + * @apiSchema (query) jobAuditMessagesQuerySchema */ router.get( { path: '/api/ml/job_audit_messages/messages', validate: { - params: schema.object({ jobId: schema.maybe(schema.string()) }), - query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), + query: jobAuditMessagesQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 718f9e81603b1..493974cbafe36 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -53,12 +53,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/force_start_datafeeds Start datafeeds * @apiName ForceStartDatafeeds * @apiDescription Starts one or more datafeeds + * + * @apiSchema (body) forceStartDatafeedSchema */ router.post( { path: '/api/ml/jobs/force_start_datafeeds', validate: { - body: schema.object(forceStartDatafeedSchema), + body: forceStartDatafeedSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -82,12 +84,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/stop_datafeeds Stop datafeeds * @apiName StopDatafeeds * @apiDescription Stops one or more datafeeds + * + * @apiSchema (body) datafeedIdsSchema */ router.post( { path: '/api/ml/jobs/stop_datafeeds', validate: { - body: schema.object(datafeedIdsSchema), + body: datafeedIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -111,12 +115,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/delete_jobs Delete jobs * @apiName DeleteJobs * @apiDescription Deletes an existing anomaly detection job + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/delete_jobs', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -140,12 +146,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/close_jobs Close jobs * @apiName CloseJobs * @apiDescription Closes one or more anomaly detection jobs + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/close_jobs', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -169,12 +177,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs_summary Jobs summary * @apiName JobsSummary * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/jobs_summary', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -198,6 +208,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs_with_time_range Jobs with time range * @apiName JobsWithTimeRange * @apiDescription Creates a list of jobs with data about the job's time range + * + * @apiSchema (body) jobsWithTimerangeSchema */ router.post( { @@ -226,12 +238,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs Create jobs list * @apiName CreateFullJobsList * @apiDescription Creates a list of jobs + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/jobs', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -281,6 +295,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/update_groups Update job groups * @apiName UpdateGroups * @apiDescription Updates 'groups' property of an anomaly detection job + * + * @apiSchema (body) updateGroupsSchema */ router.post( { @@ -336,12 +352,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs_exist Check if jobs exist * @apiName JobsExist * @apiDescription Checks if each of the jobs in the specified list of IDs exist + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/jobs_exist', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -397,6 +415,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/new_job_line_chart Get job line chart data * @apiName NewJobLineChart * @apiDescription Returns line chart data for anomaly detection job + * + * @apiSchema (body) chartSchema */ router.post( { @@ -447,6 +467,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/new_job_population_chart Get population job chart data * @apiName NewJobPopulationChart * @apiDescription Returns population job chart data + * + * @apiSchema (body) chartSchema */ router.post( { @@ -523,6 +545,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/look_back_progress Get lookback progress * @apiName GetLookBackProgress * @apiDescription Returns current progress of anomaly detection job + * + * @apiSchema (body) lookBackProgressSchema */ router.post( { @@ -552,6 +576,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/categorization_field_examples Get categorization field examples * @apiName ValidateCategoryExamples * @apiDescription Validates category examples + * + * @apiSchema (body) categorizationFieldExamplesSchema */ router.post( { @@ -611,6 +637,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/top_categories Get top categories * @apiName TopCategories * @apiDescription Returns list of top categories + * + * @apiSchema (body) topCategoriesSchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 75d9cdf375049..dd2bd9deadf43 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'kibana/server'; -import { schema, TypeOf } from '@kbn/config-schema'; +import { TypeOf } from '@kbn/config-schema'; import { AnalysisConfig } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; @@ -48,6 +48,8 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @api {post} /api/ml/validate/estimate_bucket_span Estimate bucket span * @apiName EstimateBucketSpan * @apiDescription Estimates minimum viable bucket span based on the characteristics of a pre-viewed subset of the data + * + * @apiSchema (body) estimateBucketSpanSchema */ router.post( { @@ -94,6 +96,8 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @apiName CalculateModelMemoryLimit * @apiDescription Calls _estimate_model_memory endpoint to retrieve model memory estimation. * + * @apiSchema (body) modelMemoryLimitSchema + * * @apiSuccess {String} modelMemoryLimit */ router.post( @@ -122,12 +126,14 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @api {post} /api/ml/validate/cardinality Validate cardinality * @apiName ValidateCardinality * @apiDescription Validates cardinality for the given job configuration + * + * @apiSchema (body) validateCardinalitySchema */ router.post( { path: '/api/ml/validate/cardinality', validate: { - body: schema.object(validateCardinalitySchema), + body: validateCardinalitySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -152,6 +158,8 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @api {post} /api/ml/validate/job Validates job * @apiName ValidateJob * @apiDescription Validates the given job configuration + * + * @apiSchema (body) validateJobSchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 358cd0ac2871c..2d462b6dc207a 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -152,7 +152,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { * @apiName SetupModule * @apiDescription Created module items. * - * @apiParam {String} moduleId Module id + * @apiSchema (body) setupModuleBodySchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 9849410eaf0d4..89c267340fe52 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -5,7 +5,6 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -80,12 +79,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/anomalies_table_data Prepare anomalies records for table display * @apiName GetAnomaliesTableData * @apiDescription Retrieves anomaly records for an anomaly detection job and formats them for anomalies table display + * + * @apiSchema (body) anomaliesTableDataSchema */ router.post( { path: '/api/ml/results/anomalies_table_data', validate: { - body: schema.object(anomaliesTableDataSchema), + body: anomaliesTableDataSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -107,12 +108,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/category_definition Returns category definition * @apiName GetCategoryDefinition * @apiDescription Returns the definition of the category with the specified ID and job ID + * + * @apiSchema (body) categoryDefinitionSchema */ router.post( { path: '/api/ml/results/category_definition', validate: { - body: schema.object(categoryDefinitionSchema), + body: categoryDefinitionSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -134,12 +137,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/max_anomaly_score Returns the maximum anomaly_score * @apiName GetMaxAnomalyScore * @apiDescription Returns the maximum anomaly score of the bucket results for the request job ID(s) and time range + * + * @apiSchema (body) maxAnomalyScoreSchema */ router.post( { path: '/api/ml/results/max_anomaly_score', validate: { - body: schema.object(maxAnomalyScoreSchema), + body: maxAnomalyScoreSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -161,12 +166,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/category_examples Returns category examples * @apiName GetCategoryExamples * @apiDescription Returns examples for the categories with the specified IDs from the job with the supplied ID + * + * @apiSchema (body) categoryExamplesSchema */ router.post( { path: '/api/ml/results/category_examples', validate: { - body: schema.object(categoryExamplesSchema), + body: categoryExamplesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -188,12 +195,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/partition_fields_values Returns partition fields values * @apiName GetPartitionFieldsValues * @apiDescription Returns the partition fields with values that match the provided criteria for the specified job ID. + * + * @apiSchema (body) partitionFieldValuesSchema */ router.post( { path: '/api/ml/results/partition_fields_values', validate: { - body: schema.object(partitionFieldValuesSchema), + body: partitionFieldValuesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts index 7d3d6aabb129c..fade2093ac842 100644 --- a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const indexAnnotationSchema = { +export const indexAnnotationSchema = schema.object({ timestamp: schema.number(), end_timestamp: schema.number(), annotation: schema.string(), @@ -16,15 +16,16 @@ export const indexAnnotationSchema = { create_username: schema.maybe(schema.string()), modified_time: schema.maybe(schema.number()), modified_username: schema.maybe(schema.string()), + /** Document id */ _id: schema.maybe(schema.string()), key: schema.maybe(schema.string()), -}; +}); -export const getAnnotationsSchema = { +export const getAnnotationsSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), earliestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), latestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), maxAnnotations: schema.number(), -}; +}); -export const deleteAnnotationSchema = { annotationId: schema.string() }; +export const deleteAnnotationSchema = schema.object({ annotationId: schema.string() }); diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 22c3d94dfb29e..ab1305d9bc354 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -26,6 +26,7 @@ const detectorSchema = schema.object({ over_field_name: schema.maybe(schema.string()), partition_field_name: schema.maybe(schema.string()), detector_description: schema.maybe(schema.string()), + /** Custom rules */ custom_rules: customRulesSchema, }); @@ -37,20 +38,24 @@ const customUrlSchema = { const customSettingsSchema = schema.object( { + /** Indicates the creator entity */ created_by: schema.maybe(schema.string()), - custom_urls: schema.maybe(schema.arrayOf(schema.maybe(schema.object({ ...customUrlSchema })))), + custom_urls: schema.maybe(schema.arrayOf(schema.maybe(schema.object(customUrlSchema)))), }, { unknowns: 'allow' } // Create / Update job API allows other fields to be added to custom_settings. ); -export const anomalyDetectionUpdateJobSchema = { +export const anomalyDetectionUpdateJobSchema = schema.object({ description: schema.maybe(schema.string()), detectors: schema.maybe( schema.arrayOf( schema.maybe( schema.object({ + /** Detector index */ detector_index: schema.number(), + /** Description */ description: schema.maybe(schema.string()), + /** Custom rules */ custom_rules: customRulesSchema, }) ) @@ -64,7 +69,7 @@ export const anomalyDetectionUpdateJobSchema = { }) ), groups: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), -}; +}); export const analysisConfigSchema = schema.object({ bucket_span: schema.maybe(schema.string()), @@ -78,6 +83,7 @@ export const anomalyDetectionJobSchema = { analysis_config: analysisConfigSchema, analysis_limits: schema.maybe( schema.object({ + /** Limit of categorization examples */ categorization_examples_limit: schema.maybe(schema.number()), model_memory_limit: schema.maybe(schema.string()), }) @@ -88,6 +94,7 @@ export const anomalyDetectionJobSchema = { allow_lazy_open: schema.maybe(schema.any()), data_counts: schema.maybe(schema.any()), data_description: schema.object({ + /** Format */ format: schema.maybe(schema.string()), time_field: schema.string(), time_format: schema.maybe(schema.string()), @@ -110,3 +117,63 @@ export const anomalyDetectionJobSchema = { results_retention_days: schema.maybe(schema.number()), state: schema.maybe(schema.string()), }; + +export const jobIdSchema = schema.object({ + /** Job id */ + jobId: schema.string(), +}); + +export const getRecordsSchema = schema.object({ + desc: schema.maybe(schema.boolean()), + end: schema.maybe(schema.string()), + exclude_interim: schema.maybe(schema.boolean()), + page: schema.maybe( + schema.object({ + from: schema.maybe(schema.number()), + size: schema.maybe(schema.number()), + }) + ), + record_score: schema.maybe(schema.number()), + sort: schema.maybe(schema.string()), + start: schema.maybe(schema.string()), +}); + +export const getBucketsSchema = schema.object({ + anomaly_score: schema.maybe(schema.number()), + desc: schema.maybe(schema.boolean()), + end: schema.maybe(schema.string()), + exclude_interim: schema.maybe(schema.boolean()), + expand: schema.maybe(schema.boolean()), + /** Page definition */ + page: schema.maybe( + schema.object({ + /** Page offset */ + from: schema.maybe(schema.number()), + /** Size of the page */ + size: schema.maybe(schema.number()), + }) + ), + sort: schema.maybe(schema.string()), + start: schema.maybe(schema.string()), +}); + +export const getBucketParamsSchema = schema.object({ + jobId: schema.string(), + timestamp: schema.maybe(schema.string()), +}); + +export const getOverallBucketsSchema = schema.object({ + topN: schema.number(), + bucketSpan: schema.string(), + start: schema.number(), + end: schema.number(), +}); + +export const getCategoriesSchema = schema.object({ + /** Category id */ + categoryId: schema.string(), + /** Job id */ + jobId: schema.string(), +}); + +export const forecastAnomalyDetector = schema.object({ duration: schema.any() }); diff --git a/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts b/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts index f5e59d983a9aa..6d8f94311816d 100644 --- a/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const calendarSchema = { +export const calendarSchema = schema.object({ calendar_id: schema.maybe(schema.string()), calendarId: schema.string(), job_ids: schema.arrayOf(schema.maybe(schema.string())), @@ -22,4 +22,11 @@ export const calendarSchema = { }) ) ), -}; +}); + +export const calendarIdSchema = schema.object({ calendarId: schema.string() }); + +export const calendarIdsSchema = schema.object({ + /** Comma-separated list of calendar IDs */ + calendarIds: schema.string(), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 21454fa884b82..f1d4947a7abc5 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const dataAnalyticsJobConfigSchema = { +export const dataAnalyticsJobConfigSchema = schema.object({ description: schema.maybe(schema.string()), dest: schema.object({ index: schema.string(), @@ -17,7 +17,9 @@ export const dataAnalyticsJobConfigSchema = { query: schema.maybe(schema.any()), _source: schema.maybe( schema.object({ + /** Fields to include in results */ includes: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), + /** Fields to exclude from results */ excludes: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), }) ), @@ -26,9 +28,9 @@ export const dataAnalyticsJobConfigSchema = { analysis: schema.any(), analyzed_fields: schema.any(), model_memory_limit: schema.string(), -}; +}); -export const dataAnalyticsEvaluateSchema = { +export const dataAnalyticsEvaluateSchema = schema.object({ index: schema.string(), query: schema.maybe(schema.any()), evaluation: schema.maybe( @@ -37,15 +39,27 @@ export const dataAnalyticsEvaluateSchema = { classification: schema.maybe(schema.any()), }) ), -}; +}); -export const dataAnalyticsExplainSchema = { +export const dataAnalyticsExplainSchema = schema.object({ description: schema.maybe(schema.string()), dest: schema.maybe(schema.any()), + /** Source */ source: schema.object({ index: schema.string(), }), analysis: schema.any(), analyzed_fields: schema.maybe(schema.any()), model_memory_limit: schema.maybe(schema.string()), -}; +}); + +export const analyticsIdSchema = schema.object({ + /** + * Analytics ID + */ + analyticsId: schema.string(), +}); + +export const stopsDataFrameAnalyticsJobQuerySchema = schema.object({ + force: schema.maybe(schema.boolean()), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts index 0c10b2d5b4f16..1a1d02f991b55 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts @@ -6,33 +6,27 @@ import { schema } from '@kbn/config-schema'; -export const dataVisualizerFieldStatsSchema = { - params: schema.object({ - indexPatternTitle: schema.string(), - }), - body: schema.object({ - query: schema.any(), - fields: schema.arrayOf(schema.any()), - samplerShardSize: schema.number(), - timeFieldName: schema.maybe(schema.string()), - earliest: schema.maybe(schema.number()), - latest: schema.maybe(schema.number()), - interval: schema.maybe(schema.string()), - maxExamples: schema.number(), - }), -}; +export const indexPatternTitleSchema = schema.object({ + indexPatternTitle: schema.string(), +}); -export const dataVisualizerOverallStatsSchema = { - params: schema.object({ - indexPatternTitle: schema.string(), - }), - body: schema.object({ - query: schema.any(), - aggregatableFields: schema.arrayOf(schema.string()), - nonAggregatableFields: schema.arrayOf(schema.string()), - samplerShardSize: schema.number(), - timeFieldName: schema.maybe(schema.string()), - earliest: schema.maybe(schema.number()), - latest: schema.maybe(schema.number()), - }), -}; +export const dataVisualizerFieldStatsSchema = schema.object({ + query: schema.any(), + fields: schema.arrayOf(schema.any()), + samplerShardSize: schema.number(), + timeFieldName: schema.maybe(schema.string()), + earliest: schema.maybe(schema.number()), + latest: schema.maybe(schema.number()), + interval: schema.maybe(schema.string()), + maxExamples: schema.number(), +}); + +export const dataVisualizerOverallStatsSchema = schema.object({ + query: schema.any(), + aggregatableFields: schema.arrayOf(schema.string()), + nonAggregatableFields: schema.arrayOf(schema.string()), + samplerShardSize: schema.number(), + timeFieldName: schema.maybe(schema.string()), + earliest: schema.maybe(schema.number()), + latest: schema.maybe(schema.number()), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts index 466e70197e3d1..2cfb9d7d275d5 100644 --- a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts @@ -42,3 +42,9 @@ export const datafeedConfigSchema = schema.object({ }) ), }); + +export const datafeedIdSchema = schema.object({ datafeedId: schema.string() }); + +export const deleteDatafeedQuerySchema = schema.maybe( + schema.object({ force: schema.maybe(schema.any()) }) +); diff --git a/x-pack/plugins/ml/server/routes/schemas/file_data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/file_data_visualizer_schema.ts new file mode 100644 index 0000000000000..9a80cf795cabf --- /dev/null +++ b/x-pack/plugins/ml/server/routes/schemas/file_data_visualizer_schema.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const analyzeFileQuerySchema = schema.maybe( + schema.object({ + charset: schema.maybe(schema.string()), + column_names: schema.maybe(schema.string()), + delimiter: schema.maybe(schema.string()), + explain: schema.maybe(schema.string()), + format: schema.maybe(schema.string()), + grok_pattern: schema.maybe(schema.string()), + has_header_row: schema.maybe(schema.string()), + line_merge_size_limit: schema.maybe(schema.string()), + lines_to_sample: schema.maybe(schema.string()), + quote: schema.maybe(schema.string()), + should_trim_fields: schema.maybe(schema.string()), + timeout: schema.maybe(schema.string()), + timestamp_field: schema.maybe(schema.string()), + timestamp_format: schema.maybe(schema.string()), + }) +); + +export const importFileQuerySchema = schema.object({ + id: schema.maybe(schema.string()), +}); + +export const importFileBodySchema = schema.object({ + index: schema.maybe(schema.string()), + data: schema.arrayOf(schema.any()), + settings: schema.maybe(schema.any()), + /** Mappings */ + mappings: schema.any(), + /** Ingest pipeline definition */ + ingestPipeline: schema.object({ + id: schema.maybe(schema.string()), + pipeline: schema.maybe(schema.any()), + }), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts b/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts index dffee56565c73..d33d34c7096ce 100644 --- a/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts @@ -6,14 +6,21 @@ import { schema } from '@kbn/config-schema'; -export const createFilterSchema = { +export const createFilterSchema = schema.object({ filterId: schema.string(), description: schema.maybe(schema.string()), items: schema.arrayOf(schema.string()), -}; +}); -export const updateFilterSchema = { +export const updateFilterSchema = schema.object({ description: schema.maybe(schema.string()), addItems: schema.maybe(schema.arrayOf(schema.string())), removeItems: schema.maybe(schema.arrayOf(schema.string())), -}; +}); + +export const filterIdSchema = schema.object({ + /** + * ID of the filter + */ + filterId: schema.string(), +}); diff --git a/x-pack/plugins/reporting/config.ts b/x-pack/plugins/ml/server/routes/schemas/indices_schema.ts similarity index 55% rename from x-pack/plugins/reporting/config.ts rename to x-pack/plugins/ml/server/routes/schemas/indices_schema.ts index f1d6b1a8f248f..f1b06392292f0 100644 --- a/x-pack/plugins/reporting/config.ts +++ b/x-pack/plugins/ml/server/routes/schemas/indices_schema.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; -export const reportingPollConfig = { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, -}; +export const indicesSchema = schema.object({ + index: schema.maybe(schema.string()), + fields: schema.maybe(schema.arrayOf(schema.string())), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts new file mode 100644 index 0000000000000..b94a004384eb1 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.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 { schema } from '@kbn/config-schema'; + +export const jobIdSchema = schema.object({ jobId: schema.maybe(schema.string()) }); + +export const jobAuditMessagesQuerySchema = schema.maybe( + schema.object({ from: schema.maybe(schema.any()) }) +); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index deb62678a777c..d2036b8a7c0fa 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -29,21 +29,25 @@ export const chartSchema = { splitFieldValue: schema.maybe(schema.nullable(schema.string())), }; -export const datafeedIdsSchema = { datafeedIds: schema.arrayOf(schema.maybe(schema.string())) }; +export const datafeedIdsSchema = schema.object({ + datafeedIds: schema.arrayOf(schema.maybe(schema.string())), +}); -export const forceStartDatafeedSchema = { +export const forceStartDatafeedSchema = schema.object({ datafeedIds: schema.arrayOf(schema.maybe(schema.string())), start: schema.maybe(schema.number()), end: schema.maybe(schema.number()), -}; +}); -export const jobIdsSchema = { +export const jobIdsSchema = schema.object({ jobIds: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) ), -}; +}); -export const jobsWithTimerangeSchema = { dateFormatTz: schema.maybe(schema.string()) }; +export const jobsWithTimerangeSchema = { + dateFormatTz: schema.maybe(schema.string()), +}; export const lookBackProgressSchema = { jobId: schema.string(), diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index 3ded6e770eed5..f12c85962a28d 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -37,7 +37,7 @@ export const validateJobSchema = schema.object({ job: schema.object(anomalyDetectionJobSchema), }); -export const validateCardinalitySchema = { +export const validateCardinalitySchema = schema.object({ ...anomalyDetectionJobSchema, datafeed_config: datafeedConfigSchema, -}; +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index 32d829db7f81b..f7317e534b33b 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -12,7 +12,7 @@ const criteriaFieldSchema = schema.object({ fieldValue: schema.any(), }); -export const anomaliesTableDataSchema = { +export const anomaliesTableDataSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), criteriaFields: schema.arrayOf(criteriaFieldSchema), influencers: schema.arrayOf( @@ -26,29 +26,29 @@ export const anomaliesTableDataSchema = { maxRecords: schema.number(), maxExamples: schema.maybe(schema.number()), influencersFilterQuery: schema.maybe(schema.any()), -}; +}); -export const categoryDefinitionSchema = { +export const categoryDefinitionSchema = schema.object({ jobId: schema.maybe(schema.string()), categoryId: schema.string(), -}; +}); -export const maxAnomalyScoreSchema = { +export const maxAnomalyScoreSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), earliestMs: schema.maybe(schema.number()), latestMs: schema.maybe(schema.number()), -}; +}); -export const categoryExamplesSchema = { +export const categoryExamplesSchema = schema.object({ jobId: schema.string(), categoryIds: schema.arrayOf(schema.string()), maxExamples: schema.number(), -}; +}); -export const partitionFieldValuesSchema = { +export const partitionFieldValuesSchema = schema.object({ jobId: schema.string(), searchTerm: schema.maybe(schema.any()), criteriaFields: schema.arrayOf(criteriaFieldSchema), earliestMs: schema.number(), latestMs: schema.number(), -}; +}); diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts index be08338807dd0..9342c2574bedd 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts @@ -17,12 +17,12 @@ import { export function registerTasks({ taskManager, logger, - elasticsearch, + getStartServices, config, }: { taskManager?: TaskManagerSetupContract; logger: Logger; - elasticsearch: CoreSetup['elasticsearch']; + getStartServices: CoreSetup['getStartServices']; config: Observable<{ kibana: { index: string } }>; }) { if (!taskManager) { @@ -30,13 +30,17 @@ export function registerTasks({ return; } + const esClientPromise = getStartServices().then( + ([{ elasticsearch }]) => elasticsearch.legacy.client + ); + taskManager.registerTaskDefinitions({ [VIS_TELEMETRY_TASK]: { title: 'X-Pack telemetry calculator for Visualizations', type: VIS_TELEMETRY_TASK, createTaskRunner({ taskInstance }: { taskInstance: TaskInstance }) { return { - run: visualizationsTaskRunner(taskInstance, config, elasticsearch), + run: visualizationsTaskRunner(taskInstance, config, esClientPromise), }; }, }, diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts index dff5e24db3c6e..6a47983a6f4d9 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { getMockCallWithInternal, getMockConfig, @@ -13,6 +12,7 @@ import { } from '../../../test_utils'; import { visualizationsTaskRunner } from './task_runner'; import { TaskInstance } from '../../../../../task_manager/server'; +import { getNextMidnight } from '../../get_next_midnight'; describe('visualizationsTaskRunner', () => { let mockTaskInstance: TaskInstance; @@ -41,12 +41,6 @@ describe('visualizationsTaskRunner', () => { }); test('Summarizes visualization response data', async () => { - const getNextMidnight = () => - moment() - .add(1, 'days') - .startOf('day') - .toDate(); - const runner = visualizationsTaskRunner(mockTaskInstance, getMockConfig(), getMockEs()); const result = await runner(); diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts index 556dc465e562d..f60c44e548f3f 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; import _, { countBy, groupBy, mapValues } from 'lodash'; -import { APICaller, CoreSetup } from 'kibana/server'; +import { APICaller, IClusterClient } from 'src/core/server'; import { getNextMidnight } from '../../get_next_midnight'; import { TaskInstance } from '../../../../../task_manager/server'; import { ESSearchHit } from '../../../../../apm/typings/elasticsearch'; @@ -73,17 +73,15 @@ async function getStats(callCluster: APICaller, index: string) { export function visualizationsTaskRunner( taskInstance: TaskInstance, config: Observable<{ kibana: { index: string } }>, - es: CoreSetup['elasticsearch'] + esClientPromise: Promise ) { - const { callAsInternalUser: callCluster } = es.createClient('data'); - return async () => { let stats; let error; try { const index = (await config.toPromise()).kibana.index; - stats = await getStats(callCluster, index); + stats = await getStats((await esClientPromise).callAsInternalUser, index); } catch (err) { if (err.constructor === Error) { error = err.message; diff --git a/x-pack/plugins/oss_telemetry/server/plugin.ts b/x-pack/plugins/oss_telemetry/server/plugin.ts index 430fca2d39837..6a447da66952a 100644 --- a/x-pack/plugins/oss_telemetry/server/plugin.ts +++ b/x-pack/plugins/oss_telemetry/server/plugin.ts @@ -35,7 +35,7 @@ export class OssTelemetryPlugin implements Plugin { registerTasks({ taskManager: deps.taskManager, logger: this.logger, - elasticsearch: core.elasticsearch, + getStartServices: core.getStartServices, config: this.config, }); registerCollectors( diff --git a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts index fc8c98c164623..4d703db3dcc64 100644 --- a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts +++ b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller, CoreSetup } from 'kibana/server'; +import { APICaller } from 'kibana/server'; import { of } from 'rxjs'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { ConcreteTaskInstance, TaskStatus, @@ -43,10 +44,11 @@ const defaultMockSavedObjects = [ const defaultMockTaskDocs = [getMockTaskInstance()]; -export const getMockEs = (mockCallWithInternal: APICaller = getMockCallWithInternal()) => - (({ - createClient: () => ({ callAsInternalUser: mockCallWithInternal }), - } as unknown) as CoreSetup['elasticsearch']); +export const getMockEs = async (mockCallWithInternal: APICaller = getMockCallWithInternal()) => { + const client = elasticsearchServiceMock.createClusterClient(); + (client.callAsInternalUser as any) = mockCallWithInternal; + return client; +}; export const getMockCallWithInternal = (hits: unknown[] = defaultMockSavedObjects): APICaller => { return ((() => { diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index b86f16228878a..fca4a5dbc5f94 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -29,15 +29,9 @@ export class RemoteClustersServerPlugin implements Plugin this.licenseStatus = { valid: false }; } - async setup( - { http, elasticsearch: elasticsearchService }: CoreSetup, - { licensing, cloud }: Dependencies - ) { - const elasticsearch = await elasticsearchService.adminClient; + async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, router, getLicenseStatus: () => this.licenseStatus, config: { diff --git a/x-pack/plugins/remote_clusters/server/types.ts b/x-pack/plugins/remote_clusters/server/types.ts index 85678cba92f19..23f4ed158c2d4 100644 --- a/x-pack/plugins/remote_clusters/server/types.ts +++ b/x-pack/plugins/remote_clusters/server/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'kibana/server'; +import { IRouter } from 'kibana/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { CloudSetup } from '../../cloud/server'; @@ -16,8 +16,6 @@ export interface Dependencies { export interface RouteDependencies { router: IRouter; getLicenseStatus: () => LicenseStatus; - elasticsearchService: ElasticsearchServiceSetup; - elasticsearch: IClusterClient; config: { isCloudEnabled: boolean; }; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 8f47a0a6b2ac1..5756d29face12 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -24,6 +24,7 @@ export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporti // Statuses export const JOB_STATUS_FAILED = 'failed'; export const JOB_STATUS_COMPLETED = 'completed'; +export const JOB_STATUS_WARNINGS = 'completed_with_warnings'; export enum JobStatuses { PENDING = 'pending', @@ -31,6 +32,7 @@ export enum JobStatuses { COMPLETED = 'completed', FAILED = 'failed', CANCELLED = 'cancelled', + WARNINGS = 'completed_with_warnings', } // Types diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 7c1a2ebd7d9de..26d661e29bd94 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -14,7 +14,12 @@ import { } from '../../../src/core/public'; export type JobId = string; -export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed'; +export type JobStatus = + | 'completed' + | 'completed_with_warnings' + | 'pending' + | 'processing' + | 'failed'; export type HttpService = HttpSetup; export type NotificationsService = NotificationsStart; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 3da2d2a094706..d068711b87c9d 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,6 +2,9 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", + "optionalPlugins": [ + "usageCollection" + ], "configPath": ["xpack", "reporting"], "requiredPlugins": [ "home", @@ -12,6 +15,6 @@ "share", "kibanaLegacy" ], - "server": false, + "server": true, "ui": true } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx index b0674c149609d..6c13264ebcb1f 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx @@ -14,7 +14,7 @@ type Props = { record: ListingJob } & ListingProps; export const ReportDownloadButton: FunctionComponent = (props: Props) => { const { record, apiClient, intl } = props; - if (record.status !== JobStatuses.COMPLETED) { + if (record.status !== JobStatuses.COMPLETED && record.status !== JobStatuses.WARNINGS) { return null; } diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx index 9b541261a690b..380a3b3295b9f 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -17,17 +17,16 @@ import { ReportListing } from './report_listing'; const reportingAPIClient = { list: () => Promise.resolve([ - { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore - ]), + { _id: 'k90e51pk1ieucbae0c3t8wo2', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T21:01:13.062Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000, }, sort: [1586898073064], }, // prettier-ignore + { _id: 'k90e51pk1ieucbae0c3t8wo1', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T21:01:13.062Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000, }, sort: [1586898073064], }, + { _id: 'k90cmthd1gv8cbae0c2le8bo', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T20:19:02.976Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000, }, sort: [1586895542977], }, + { _id: 'k906958e1d4wcbae0c9hip1a', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', ], }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:20:27.326Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e8-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000, }, sort: [1586884827326], }, + { _id: 'k9067y2a1d4wcbae0cad38n0', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:19:31.378Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000, }, sort: [1586884771379], }, + { _id: 'k9067s1m1d4wcbae0cdnvcms', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:19:23.578Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000, }, sort: [1586884763578], }, + { _id: 'k9065q3s1d4wcbae0c00fxlh', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:17:47.750Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000, }, sort: [1586884667752], }, + { _id: 'k905zdw11d34cbae0c3y6tzh', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:12:51.984Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000, }, sort: [1586884371985], }, + { _id: 'k8t4ylcb07mi9d006214ifyg', _index: '.reporting-2020.04.05', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization', }, output: { content_type: 'image/png', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-09T19:09:52.137Z', layout: { dimensions: { height: 1575, width: 1423, }, id: 'png', }, objectType: 'visualization', relativeUrl: "/s/hsyjklk/app/kibana#/visualize/edit/94d1fe40-7a94-11ea-b373-0749f92ad295?_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!((enabled:!t,id:'1',params:(),schema:metric,type:count)),params:(addLegend:!f,addTooltip:!t,metric:(colorSchema:'Green%20to%20Red',colorsRange:!((from:0,to:10000)),invertColors:!f,labels:(show:!t),metricColorMode:None,percentageMode:!f,style:(bgColor:!f,bgFill:%23000,fontSize:60,labelColor:!f,subText:''),useRanges:!f),type:metric),title:count,type:metric))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&indexPattern=d81752b0-7434-11ea-be36-1f978cda44d4&type=metric", title: 'count', }, priority: 10, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000, }, sort: [1586459392139], }, + ]), // prettier-ignore total: () => Promise.resolve(18), } as any; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 93dd293876f82..885e9577471a0 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -87,6 +87,12 @@ const jobStatusLabelsMap = new Map([ defaultMessage: 'Completed', }), ], + [ + JobStatuses.WARNINGS, + i18n.translate('xpack.reporting.jobStatuses.warningText', { + defaultMessage: 'Completed with warnings', + }), + ], [ JobStatuses.FAILED, i18n.translate('xpack.reporting.jobStatuses.failedText', { @@ -410,7 +416,11 @@ class ReportListingUi extends Component { statusTimestamp = this.formatDate(record.started_at); } else if ( record.completed_at && - (status === JobStatuses.COMPLETED || status === JobStatuses.FAILED) + ([ + JobStatuses.COMPLETED, + JobStatuses.FAILED, + JobStatuses.WARNINGS, + ] as string[]).includes(status) ) { statusTimestamp = this.formatDate(record.completed_at); } diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 1aae30f6fdfb0..3c121f1712685 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -11,6 +11,7 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, + JOB_STATUS_WARNINGS, } from '../../constants'; import { @@ -112,7 +113,7 @@ export class ReportingNotifierStreamHandler { _source: { status: jobStatus }, } = job; if (storedJobs.includes(jobId)) { - if (jobStatus === JOB_STATUS_COMPLETED) { + if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) { completedJobs.push(summarizeJob(job)); } else if (jobStatus === JOB_STATUS_FAILED) { failedJobs.push(summarizeJob(job)); diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts new file mode 100644 index 0000000000000..3107866be6496 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Rx from 'rxjs'; +import { CoreSetup, Logger, PluginInitializerContext } from 'src/core/server'; +import { ConfigType as ReportingConfigType } from './schema'; +import { createConfig$ } from './create_config'; + +interface KibanaServer { + host?: string; + port?: number; + protocol?: string; +} + +const makeMockInitContext = (config: { + capture?: Partial; + encryptionKey?: string; + kibanaServer: Partial; +}): PluginInitializerContext => + ({ + config: { + create: () => + Rx.of({ + ...config, + capture: config.capture || { browser: { chromium: { disableSandbox: false } } }, + kibanaServer: config.kibanaServer || {}, + }), + }, + } as PluginInitializerContext); + +const makeMockCoreSetup = (serverInfo: KibanaServer): CoreSetup => + ({ http: { getServerInfo: () => serverInfo } } as any); + +describe('Reporting server createConfig$', () => { + let mockCoreSetup: CoreSetup; + let mockInitContext: PluginInitializerContext; + let mockLogger: Logger; + + beforeEach(() => { + mockCoreSetup = makeMockCoreSetup({ host: 'kibanaHost', port: 5601, protocol: 'http' }); + mockInitContext = makeMockInitContext({ + kibanaServer: {}, + }); + mockLogger = ({ warn: jest.fn(), debug: jest.fn() } as unknown) as Logger; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('creates random encryption key and default config using host, protocol, and port from server info', async () => { + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.encryptionKey).toMatch(/\S{32,}/); // random 32 characters + expect(result.kibanaServer).toMatchInlineSnapshot(` + Object { + "hostname": "kibanaHost", + "port": 5601, + "protocol": "http", + } + `); + expect((mockLogger.warn as any).mock.calls.length).toBe(1); + expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([ + 'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in kibana.yml', + ]); + }); + + it('uses the user-provided encryption key', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii', + kibanaServer: {}, + }); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.encryptionKey).toMatch('iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii'); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('uses the user-provided encryption key, reporting kibanaServer settings to override server info', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii', + kibanaServer: { + hostname: 'reportingHost', + port: 5677, + protocol: 'httpsa', + }, + }); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "chromium": Object { + "disableSandbox": false, + }, + }, + }, + "encryptionKey": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii", + "kibanaServer": Object { + "hostname": "reportingHost", + "port": 5677, + "protocol": "httpsa", + }, + } + `); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('show warning when kibanaServer.hostName === "0"', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: 'aaaaaaaaaaaaabbbbbbbbbbbbaaaaaaaaa', + kibanaServer: { hostname: '0' }, + }); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.kibanaServer).toMatchInlineSnapshot(` + Object { + "hostname": "0.0.0.0", + "port": 5601, + "protocol": "http", + } + `); + expect((mockLogger.warn as any).mock.calls.length).toBe(1); + expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([ + `Found 'server.host: \"0\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically ` + + `to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.`, + ]); + }); + + it('uses user-provided disableSandbox: false', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: '888888888888888888888888888888888', + capture: { browser: { chromium: { disableSandbox: false } } }, + } as ReportingConfigType); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: false }); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('uses user-provided disableSandbox: true', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: '888888888888888888888888888888888', + capture: { browser: { chromium: { disableSandbox: true } } }, + } as ReportingConfigType); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: true }); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('provides a default for disableSandbox', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: '888888888888888888888888888888888', + } as ReportingConfigType); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: expect.any(Boolean) }); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); +}); diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts new file mode 100644 index 0000000000000..1e6e8bbde5d27 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n/'; +import { TypeOf } from '@kbn/config-schema'; +import crypto from 'crypto'; +import { capitalize } from 'lodash'; +import { map, mergeMap } from 'rxjs/operators'; +import { CoreSetup, Logger, PluginInitializerContext } from 'src/core/server'; +import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; +import { ConfigSchema } from './schema'; + +/* + * Set up dynamic config defaults + * - xpack.capture.browser.chromium.disableSandbox + * - xpack.kibanaServer + * - xpack.reporting.encryptionKey + */ +export function createConfig$(core: CoreSetup, context: PluginInitializerContext, logger: Logger) { + return context.config.create>().pipe( + map(config => { + // encryption key + let encryptionKey = config.encryptionKey; + if (encryptionKey === undefined) { + logger.warn( + i18n.translate('xpack.reporting.serverConfig.randomEncryptionKey', { + defaultMessage: + 'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on ' + + 'restart, please set xpack.reporting.encryptionKey in kibana.yml', + }) + ); + encryptionKey = crypto.randomBytes(16).toString('hex'); + } + const { kibanaServer: reportingServer } = config; + const serverInfo = core.http.getServerInfo(); + // kibanaServer.hostname, default to server.host, don't allow "0" + let kibanaServerHostname = reportingServer.hostname + ? reportingServer.hostname + : serverInfo.host; + if (kibanaServerHostname === '0') { + logger.warn( + i18n.translate('xpack.reporting.serverConfig.invalidServerHostname', { + defaultMessage: + `Found 'server.host: "0"' in Kibana configuration. This is incompatible with Reporting. ` + + `To enable Reporting to work, '{configKey}: 0.0.0.0' is being automatically to the configuration. ` + + `You can change the setting to 'server.host: 0.0.0.0' or add '{configKey}: 0.0.0.0' in kibana.yml to prevent this message.`, + values: { configKey: 'xpack.reporting.kibanaServer.hostname' }, + }) + ); + kibanaServerHostname = '0.0.0.0'; + } + // kibanaServer.port, default to server.port + const kibanaServerPort = reportingServer.port + ? reportingServer.port + : serverInfo.port; // prettier-ignore + // kibanaServer.protocol, default to server.protocol + const kibanaServerProtocol = reportingServer.protocol + ? reportingServer.protocol + : serverInfo.protocol; + return { + ...config, + encryptionKey, + kibanaServer: { + hostname: kibanaServerHostname, + port: kibanaServerPort, + protocol: kibanaServerProtocol, + }, + }; + }), + mergeMap(async config => { + if (config.capture.browser.chromium.disableSandbox != null) { + // disableSandbox was set by user + return config; + } + + // disableSandbox was not set by user, apply default for OS + const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled(); + const osName = [os.os, os.dist, os.release] + .filter(Boolean) + .map(capitalize) + .join(' '); + + logger.debug( + i18n.translate('xpack.reporting.serverConfig.osDetected', { + defaultMessage: `Running on OS: '{osName}'`, + values: { osName }, + }) + ); + + if (disableSandbox === true) { + logger.warn( + i18n.translate('xpack.reporting.serverConfig.autoSet.sandboxDisabled', { + defaultMessage: `Chromium sandbox provides an additional layer of protection, but is not supported for {osName} OS. Automatically setting '{configKey}: true'.`, + values: { + configKey: 'xpack.reporting.capture.browser.chromium.disableSandbox', + osName, + }, + }) + ); + } else { + logger.info( + i18n.translate('xpack.reporting.serverConfig.autoSet.sandboxEnabled', { + defaultMessage: `Chromium sandbox provides an additional layer of protection, and is supported for {osName} OS. Automatically enabling Chromium sandbox.`, + values: { osName }, + }) + ); + } + + return { + ...config, + capture: { + ...config.capture, + browser: { + ...config.capture.browser, + chromium: { ...config.capture.browser.chromium, disableSandbox }, + }, + }, + }; + }) + ); +} diff --git a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.test.js b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts similarity index 82% rename from x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.test.js rename to x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts index a022506f9e2da..307c96bb34909 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.test.js +++ b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts @@ -11,11 +11,17 @@ jest.mock('getos', () => { import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; import getos from 'getos'; -function defaultTest(os, expectedDefault) { +interface TestObject { + os: string; + dist?: string; + release?: string; +} + +function defaultTest(os: TestObject, expectedDefault: boolean) { test(`${expectedDefault ? 'disabled' : 'enabled'} on ${JSON.stringify(os)}`, async () => { - getos.mockImplementation(cb => cb(null, os)); + (getos as jest.Mock).mockImplementation(cb => cb(null, os)); const actualDefault = await getDefaultChromiumSandboxDisabled(); - expect(actualDefault).toBe(expectedDefault); + expect(actualDefault.disableSandbox).toBe(expectedDefault); }); } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.ts b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts similarity index 80% rename from x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.ts rename to x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts index a21a4b33722ff..525041d5c772b 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.ts +++ b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts @@ -31,12 +31,17 @@ const distroSupportsUnprivilegedUsernamespaces = (distro: string) => { return true; }; -export async function getDefaultChromiumSandboxDisabled() { +interface OsSummary { + disableSandbox: boolean; + os: { os: string; dist?: string; release?: string }; +} + +export async function getDefaultChromiumSandboxDisabled(): Promise { const os = await getos(); if (os.os === 'linux' && !distroSupportsUnprivilegedUsernamespaces(os.dist)) { - return true; + return { os, disableSandbox: true }; } else { - return false; + return { os, disableSandbox: false }; } } diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts new file mode 100644 index 0000000000000..f0a0a093aa8c0 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { PluginConfigDescriptor } from 'kibana/server'; +import { ConfigSchema, ConfigType } from './schema'; + +export { createConfig$ } from './create_config'; + +export const config: PluginConfigDescriptor = { + schema: ConfigSchema, + deprecations: ({ unused }) => [ + unused('capture.browser.chromium.maxScreenshotDimension'), + unused('capture.concurrency'), + unused('capture.settleTime'), + unused('capture.timeout'), + unused('kibanaApp'), + ], +}; + +export { ConfigSchema, ConfigType }; diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts new file mode 100644 index 0000000000000..41285c2bfa133 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConfigSchema } from './schema'; + +describe('Reporting Config Schema', () => { + it(`context {"dev":false,"dist":false} produces correct config`, () => { + expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchObject({ + capture: { + browser: { + autoDownload: true, + chromium: { proxy: { enabled: false } }, + type: 'chromium', + }, + loadDelay: 3000, + maxAttempts: 1, + networkPolicy: { + enabled: true, + rules: [ + { allow: true, host: undefined, protocol: 'http:' }, + { allow: true, host: undefined, protocol: 'https:' }, + { allow: true, host: undefined, protocol: 'ws:' }, + { allow: true, host: undefined, protocol: 'wss:' }, + { allow: true, host: undefined, protocol: 'data:' }, + { allow: false, host: undefined, protocol: undefined }, + ], + }, + viewport: { height: 1200, width: 1950 }, + zoom: 2, + }, + csv: { + checkForFormulas: true, + enablePanelActionDownload: true, + maxSizeBytes: 10485760, + scroll: { duration: '30s', size: 500 }, + }, + encryptionKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + index: '.reporting', + kibanaServer: {}, + poll: { + jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, + jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, + }, + queue: { + indexInterval: 'week', + pollEnabled: true, + pollInterval: 3000, + pollIntervalErrorMultiplier: 10, + timeout: 120000, + }, + roles: { allow: ['reporting_user'] }, + }); + }); + + it(`context {"dev":false,"dist":true} produces correct config`, () => { + expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchObject({ + capture: { + browser: { + autoDownload: false, + chromium: { + inspect: false, + proxy: { enabled: false }, + }, + type: 'chromium', + }, + loadDelay: 3000, + maxAttempts: 3, + networkPolicy: { + enabled: true, + rules: [ + { allow: true, host: undefined, protocol: 'http:' }, + { allow: true, host: undefined, protocol: 'https:' }, + { allow: true, host: undefined, protocol: 'ws:' }, + { allow: true, host: undefined, protocol: 'wss:' }, + { allow: true, host: undefined, protocol: 'data:' }, + { allow: false, host: undefined, protocol: undefined }, + ], + }, + viewport: { height: 1200, width: 1950 }, + zoom: 2, + }, + csv: { + checkForFormulas: true, + enablePanelActionDownload: true, + maxSizeBytes: 10485760, + scroll: { duration: '30s', size: 500 }, + }, + index: '.reporting', + kibanaServer: {}, + poll: { + jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, + jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, + }, + queue: { + indexInterval: 'week', + pollEnabled: true, + pollInterval: 3000, + pollIntervalErrorMultiplier: 10, + timeout: 120000, + }, + roles: { allow: ['reporting_user'] }, + }); + }); + + it(`allows optional settings`, () => { + // encryption key + expect( + ConfigSchema.validate({ encryptionKey: 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq' }) + .encryptionKey + ).toBe('qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); + + // disableSandbox + expect( + ConfigSchema.validate({ capture: { browser: { chromium: { disableSandbox: true } } } }) + .capture.browser.chromium + ).toMatchObject({ disableSandbox: true, proxy: { enabled: false } }); + + // kibanaServer + expect( + ConfigSchema.validate({ kibanaServer: { hostname: 'Frodo' } }).kibanaServer + ).toMatchObject({ hostname: 'Frodo' }); + }); + + it(`logs the proper validation messages`, () => { + // kibanaServer + const throwValidationErr = () => ConfigSchema.validate({ kibanaServer: { hostname: '0' } }); + expect(throwValidationErr).toThrowError( + `[kibanaServer.hostname]: must not be "0" for the headless browser to correctly resolve the host` + ); + }); +}); diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts new file mode 100644 index 0000000000000..67d70c1513c15 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { schema, TypeOf } from '@kbn/config-schema'; +import moment from 'moment'; + +const KibanaServerSchema = schema.object({ + hostname: schema.maybe( + schema.string({ + validate(value) { + if (value === '0') { + return 'must not be "0" for the headless browser to correctly resolve the host'; + } + }, + hostname: true, + }) + ), + port: schema.maybe(schema.number()), + protocol: schema.maybe( + schema.string({ + validate(value) { + if (!/^https?$/.test(value)) { + return 'must be "http" or "https"'; + } + }, + }) + ), +}); // default values are all dynamic in createConfig$ + +const QueueSchema = schema.object({ + indexInterval: schema.string({ defaultValue: 'week' }), + pollEnabled: schema.boolean({ defaultValue: true }), + pollInterval: schema.number({ defaultValue: 3000 }), + pollIntervalErrorMultiplier: schema.number({ defaultValue: 10 }), + timeout: schema.number({ defaultValue: moment.duration(2, 'm').asMilliseconds() }), +}); + +const RulesSchema = schema.object({ + allow: schema.boolean(), + host: schema.maybe(schema.string()), + protocol: schema.maybe(schema.string()), +}); + +const CaptureSchema = schema.object({ + timeouts: schema.object({ + openUrl: schema.number({ defaultValue: 30000 }), + waitForElements: schema.number({ defaultValue: 30000 }), + renderComplete: schema.number({ defaultValue: 30000 }), + }), + networkPolicy: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + rules: schema.arrayOf(RulesSchema, { + defaultValue: [ + { host: undefined, allow: true, protocol: 'http:' }, + { host: undefined, allow: true, protocol: 'https:' }, + { host: undefined, allow: true, protocol: 'ws:' }, + { host: undefined, allow: true, protocol: 'wss:' }, + { host: undefined, allow: true, protocol: 'data:' }, + { host: undefined, allow: false, protocol: undefined }, // Default action is to deny! + ], + }), + }), + zoom: schema.number({ defaultValue: 2 }), + viewport: schema.object({ + width: schema.number({ defaultValue: 1950 }), + height: schema.number({ defaultValue: 1200 }), + }), + loadDelay: schema.number({ + defaultValue: moment.duration(3, 's').asMilliseconds(), + }), // TODO: use schema.duration + browser: schema.object({ + autoDownload: schema.conditional( + schema.contextRef('dist'), + true, + schema.boolean({ defaultValue: false }), + schema.boolean({ defaultValue: true }) + ), + chromium: schema.object({ + inspect: schema.conditional( + schema.contextRef('dist'), + true, + schema.boolean({ defaultValue: false }), + schema.maybe(schema.never()) + ), + disableSandbox: schema.maybe(schema.boolean()), // default value is dynamic in createConfig$ + proxy: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + server: schema.conditional( + schema.siblingRef('enabled'), + true, + schema.uri({ scheme: ['http', 'https'] }), + schema.maybe(schema.never()) + ), + bypass: schema.conditional( + schema.siblingRef('enabled'), + true, + schema.arrayOf(schema.string({ hostname: true })), + schema.maybe(schema.never()) + ), + }), + }), + type: schema.string({ defaultValue: 'chromium' }), + }), + maxAttempts: schema.conditional( + schema.contextRef('dist'), + true, + schema.number({ defaultValue: 3 }), + schema.number({ defaultValue: 1 }) + ), +}); + +const CsvSchema = schema.object({ + checkForFormulas: schema.boolean({ defaultValue: true }), + enablePanelActionDownload: schema.boolean({ defaultValue: true }), + maxSizeBytes: schema.number({ + defaultValue: 1024 * 1024 * 10, // 10MB + }), // TODO: use schema.byteSize + useByteOrderMarkEncoding: schema.boolean({ defaultValue: false }), + scroll: schema.object({ + duration: schema.string({ + defaultValue: '30s', + validate(value) { + if (!/^[0-9]+(d|h|m|s|ms|micros|nanos)$/.test(value)) { + return 'must be a duration string'; + } + }, + }), + size: schema.number({ defaultValue: 500 }), + }), +}); + +const EncryptionKeySchema = schema.conditional( + schema.contextRef('dist'), + true, + schema.maybe(schema.string({ minLength: 32 })), // default value is dynamic in createConfig$ + schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) +); + +const RolesSchema = schema.object({ + allow: schema.arrayOf(schema.string(), { defaultValue: ['reporting_user'] }), +}); + +const IndexSchema = schema.string({ defaultValue: '.reporting' }); + +const PollSchema = schema.object({ + jobCompletionNotifier: schema.object({ + interval: schema.number({ + defaultValue: moment.duration(10, 's').asMilliseconds(), + }), // TODO: use schema.duration + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + }), + jobsRefresh: schema.object({ + interval: schema.number({ + defaultValue: moment.duration(5, 's').asMilliseconds(), + }), // TODO: use schema.duration + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + }), +}); + +export const ConfigSchema = schema.object({ + kibanaServer: KibanaServerSchema, + queue: QueueSchema, + capture: CaptureSchema, + csv: CsvSchema, + encryptionKey: EncryptionKeySchema, + roles: RolesSchema, + index: IndexSchema, + poll: PollSchema, +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/reporting/server/index.ts b/x-pack/plugins/reporting/server/index.ts new file mode 100644 index 0000000000000..2b1844cf2e10e --- /dev/null +++ b/x-pack/plugins/reporting/server/index.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 { PluginInitializerContext } from 'src/core/server'; +import { ReportingPlugin } from './plugin'; + +export { config, ConfigSchema } from './config'; +export { ConfigType, PluginsSetup } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new ReportingPlugin(initializerContext); diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts new file mode 100644 index 0000000000000..905ed2b237c86 --- /dev/null +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; +import { ConfigType, createConfig$ } from './config'; + +export interface PluginsSetup { + /** @deprecated */ + __legacy: { + config$: Observable; + }; +} + +export class ReportingPlugin implements Plugin { + private readonly log: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.log = this.initializerContext.logger.get(); + } + + public async setup(core: CoreSetup): Promise { + return { + __legacy: { + config$: createConfig$(core, this.initializerContext, this.log).pipe(first()), + }, + }; + } + + public start() {} + public stop() {} +} + +export { ConfigType }; diff --git a/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.test.ts b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.test.ts new file mode 100644 index 0000000000000..65922ea8e6de7 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.test.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 { isTaskSavedObjectNotFoundError } from './is_task_not_found_error'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import uuid from 'uuid'; + +describe('isTaskSavedObjectNotFoundError', () => { + test('identifies SavedObjects Not Found errors', () => { + const id = uuid.v4(); + // ensure the error created by SO parses as a string with the format we expect + expect( + `${SavedObjectsErrorHelpers.createGenericNotFoundError('task', id)}`.includes(`task/${id}`) + ).toBe(true); + + const errorBySavedObjectsHelper = SavedObjectsErrorHelpers.createGenericNotFoundError( + 'task', + id + ); + + expect(isTaskSavedObjectNotFoundError(errorBySavedObjectsHelper, id)).toBe(true); + }); + + test('identifies generic errors', () => { + const id = uuid.v4(); + expect(isTaskSavedObjectNotFoundError(new Error(`not found`), id)).toBe(false); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.ts b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.ts new file mode 100644 index 0000000000000..8cc1c08f2a967 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.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 { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +export function isTaskSavedObjectNotFoundError(err: Error, taskId: string) { + return SavedObjectsErrorHelpers.isNotFoundError(err) && `${err}`.includes(taskId); +} diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index fdfe0c068afcf..e837fcd9c0dec 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -38,17 +38,16 @@ export class TaskManagerPlugin public setup(core: CoreSetup, plugins: any): TaskManagerSetupContract { const logger = this.initContext.logger.get('taskManager'); const config$ = this.initContext.config.create(); - const elasticsearch = core.elasticsearch.adminClient; return { registerLegacyAPI: once((__LEGACY: PluginLegacyDependencies) => { config$.subscribe(async config => { - const [{ savedObjects }] = await core.getStartServices(); + const [{ savedObjects, elasticsearch }] = await core.getStartServices(); const savedObjectsRepository = savedObjects.createInternalRepository(['task']); this.legacyTaskManager$.next( createTaskManager(core, { logger, config, - elasticsearch, + elasticsearch: elasticsearch.legacy.client, savedObjectsRepository, savedObjectsSerializer: savedObjects.createSerializer(), }) diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index a827c1436c9bd..fb87b6290a3da 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -8,6 +8,7 @@ import sinon from 'sinon'; import { TaskPool, TaskPoolRunResult } from './task_pool'; import { mockLogger, resolvable, sleep } from './test_utils'; import { asOk } from './lib/result_type'; +import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { @@ -101,6 +102,30 @@ describe('TaskPool', () => { expect(result).toEqual(TaskPoolRunResult.RunningAllClaimedTasks); }); + test('should not log when running a Task fails due to the Task SO having been deleted while in flight', async () => { + const logger = mockLogger(); + const pool = new TaskPool({ + maxWorkers: 3, + logger, + }); + + const taskFailedToRun = mockTask(); + taskFailedToRun.run.mockImplementation(async () => { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', taskFailedToRun.id); + }); + + const result = await pool.run([mockTask(), taskFailedToRun, mockTask()]); + + expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Task TaskType \\"shooooo\\" failed in attempt to run: Saved object [task/foo] not found", + ] + `); + expect(logger.warn).not.toHaveBeenCalled(); + + expect(result).toEqual(TaskPoolRunResult.RunningAllClaimedTasks); + }); + test('Running a task which fails still takes up capacity', async () => { const logger = mockLogger(); const pool = new TaskPool({ diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 90f1880a159da..8999fb48680ce 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -11,6 +11,7 @@ import { performance } from 'perf_hooks'; import { Logger } from './types'; import { TaskRunner } from './task_runner'; +import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; interface Opts { maxWorkers: number; @@ -125,7 +126,17 @@ export class TaskPool { taskRunner .run() .catch(err => { - this.logger.warn(`Task ${taskRunner.toString()} failed in attempt to run: ${err.message}`); + // If a task Saved Object can't be found by an in flight task runner + // we asssume the underlying task has been deleted while it was running + // so we will log this as a debug, rather than a warn + const errorLogLine = `Task ${taskRunner.toString()} failed in attempt to run: ${ + err.message + }`; + if (isTaskSavedObjectNotFoundError(err, taskRunner.id)) { + this.logger.debug(errorLogLine); + } else { + this.logger.warn(errorLogLine); + } }) .then(() => this.running.delete(taskRunner)); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a5d2c20447ad5..8e4b74c4c08fd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12431,7 +12431,6 @@ "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化", "xpack.reporting.selfCheck.ok": "レポートプラグイン自己チェックOK!", "xpack.reporting.selfCheck.warning": "レポートプラグイン自己チェックで警告が発生しました: {err}", - "xpack.reporting.selfCheckEncryptionKey.warning": "{setting}のランダムキーを生成しています。保留中のレポートの再開が失敗しないように、kibana.ymlで{setting}を設定してください", "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG レポート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ef749e9218e11..4cf18fc287cc6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12435,7 +12435,6 @@ "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化", "xpack.reporting.selfCheck.ok": "Reporting 插件自检正常!", "xpack.reporting.selfCheck.warning": "Reporting 插件自检生成警告:{err}", - "xpack.reporting.selfCheckEncryptionKey.warning": "正在为 {setting} 生成随机密钥。要防止待处理报告在重新启动时失败,请在 kibana.yml 中设置 {setting}", "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG 报告", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 5bbec1221a3ac..7557b17f5b67d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -328,6 +328,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent { setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); setAddModalVisibility(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx index 6ad52a5416163..619d85d99719b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -146,16 +146,13 @@ export const GroupByExpression = ({ {groupByTypes[groupBy].sizeRequired ? ( - 0 && termSize !== undefined} - error={errors.termSize} - > + 0} error={errors.termSize}> 0 && termSize !== undefined} - value={termSize} + isInvalid={errors.termSize.length > 0} + value={termSize || ''} onChange={e => { const { value } = e.target; - const termSizeVal = value !== '' ? parseFloat(value) : MIN_TERM_SIZE; + const termSizeVal = value !== '' ? parseFloat(value) : undefined; onChangeSelectedTermSize(termSizeVal); }} min={MIN_TERM_SIZE} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index a4833d9a3d7fe..2af1a9ac38e44 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -58,7 +58,7 @@ describe('Upgrade Assistant Usage Collector', () => { }), }, elasticsearch: { - adminClient: clusterClient, + legacy: { client: clusterClient }, }, }; }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 79d6e53c64ec0..9c2946db7f084 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -7,7 +7,7 @@ import { set } from 'lodash'; import { APICaller, - ElasticsearchServiceSetup, + ElasticsearchServiceStart, ISavedObjectsRepository, SavedObjectsServiceStart, } from 'src/core/server'; @@ -51,7 +51,7 @@ async function getDeprecationLoggingStatusValue(callAsCurrentUser: APICaller): P } export async function fetchUpgradeAssistantMetrics( - { adminClient }: ElasticsearchServiceSetup, + { legacy: { client: esClient } }: ElasticsearchServiceStart, savedObjects: SavedObjectsServiceStart ): Promise { const savedObjectsRepository = savedObjects.createInternalRepository(); @@ -60,7 +60,7 @@ export async function fetchUpgradeAssistantMetrics( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID ); - const callAsInternalUser = adminClient.callAsInternalUser.bind(adminClient); + const callAsInternalUser = esClient.callAsInternalUser.bind(esClient); const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue(callAsInternalUser); const getTelemetrySavedObject = ( @@ -107,7 +107,7 @@ export async function fetchUpgradeAssistantMetrics( } interface Dependencies { - elasticsearch: ElasticsearchServiceSetup; + elasticsearch: ElasticsearchServiceStart; savedObjects: SavedObjectsServiceStart; usageCollection: UsageCollectionSetup; } diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 6ccd073a9e020..bdca506cc7338 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -11,7 +11,6 @@ import { CoreStart, PluginInitializerContext, Logger, - ElasticsearchServiceSetup, SavedObjectsClient, SavedObjectsServiceStart, } from '../../../../src/core/server'; @@ -40,7 +39,6 @@ export class UpgradeAssistantServerPlugin implements Plugin { // Properties set at setup private licensing?: LicensingPluginSetup; - private elasticSearchService?: ElasticsearchServiceSetup; // Properties set at start private savedObjectsServiceStart?: SavedObjectsServiceStart; @@ -59,10 +57,9 @@ export class UpgradeAssistantServerPlugin implements Plugin { } setup( - { http, elasticsearch, getStartServices, capabilities }: CoreSetup, + { http, getStartServices, capabilities }: CoreSetup, { usageCollection, cloud, licensing }: PluginsSetup ) { - this.elasticSearchService = elasticsearch; this.licensing = licensing; const router = http.createRouter(); @@ -88,13 +85,13 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerTelemetryRoutes(dependencies); if (usageCollection) { - getStartServices().then(([{ savedObjects }]) => { + getStartServices().then(([{ savedObjects, elasticsearch }]) => { registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, savedObjects }); }); } } - start({ savedObjects }: CoreStart) { + start({ savedObjects, elasticsearch }: CoreStart) { this.savedObjectsServiceStart = savedObjects; // The ReindexWorker uses a map of request headers that contain the authentication credentials @@ -107,7 +104,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { this.worker = createReindexWorker({ credentialStore: this.credentialStore, licensing: this.licensing!, - elasticsearchService: this.elasticSearchService!, + elasticsearchService: elasticsearch, logger: this.logger, savedObjects: new SavedObjectsClient( this.savedObjectsServiceStart.createInternalRepository() diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index 686f93b771e62..3d15916b17ff9 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; import { - ElasticsearchServiceSetup, + ElasticsearchServiceStart, kibanaResponseFactory, Logger, SavedObjectsClient, @@ -38,7 +38,7 @@ import { GetBatchQueueResponse, PostBatchResponse } from './types'; interface CreateReindexWorker { logger: Logger; - elasticsearchService: ElasticsearchServiceSetup; + elasticsearchService: ElasticsearchServiceStart; credentialStore: CredentialStore; savedObjects: SavedObjectsClient; licensing: LicensingPluginSetup; @@ -51,8 +51,8 @@ export function createReindexWorker({ savedObjects, licensing, }: CreateReindexWorker) { - const { adminClient } = elasticsearchService; - return new ReindexWorker(savedObjects, credentialStore, adminClient, logger, licensing); + const esClient = elasticsearchService.legacy.client; + return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing); } const mapAnyErrorToKibanaHttpResponse = (e: any) => { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 4d32a5ae9f53c..457b7621e84bd 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -100,6 +100,27 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) xyzSecret2: 'credential2', }, }, + { + id: 'preconfigured-es-index-action', + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + }, + { + id: 'preconfigured.test.index-record', + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + secrets: { + encrypted: 'this-is-also-ignored-and-also-required', + }, + }, ])}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts new file mode 100644 index 0000000000000..b04bc13ffc5e4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// from: x-pack/test/alerting_api_integration/common/config.ts +const ACTION_ID = 'preconfigured-es-index-action'; +const ES_TEST_INDEX_NAME = 'functional-test-actions-index-preconfigured'; + +// eslint-disable-next-line import/no-default-export +export default function indexTest({ getService }: FtrProviderContext) { + const es = getService('legacyEs'); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('preconfigured index action', () => { + after(() => esArchiver.unload('empty_kibana')); + beforeEach(() => clearTestIndex(es)); + + it('should execute successfully when expected for a single body', async () => { + const { body: result } = await supertest + .post(`/api/action/${ACTION_ID}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + documents: [{ testing: [4, 5, 6] }], + }, + }) + .expect(200); + expect(result.status).to.eql('ok'); + + const items = await getTestIndexItems(es); + expect(items.length).to.eql(1); + + // check document sans timestamp + const document = items[0]._source; + const timestamp = document.timestamp; + delete document.timestamp; + expect(document).to.eql({ testing: [4, 5, 6] }); + + // check timestamp + const timestampTime = new Date(timestamp).getTime(); + const timeNow = Date.now(); + const timeMinuteAgo = timeNow - 1000 * 60; + expect(timestampTime).to.be.within(timeMinuteAgo, timeNow); + }); + }); +} + +async function clearTestIndex(es: any) { + return await es.indices.delete({ + index: ES_TEST_INDEX_NAME, + ignoreUnavailable: true, + }); +} + +async function getTestIndexItems(es: any) { + const result = await es.search({ + index: ES_TEST_INDEX_NAME, + }); + + return result.hits.hits; +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 80b512f3fb5e3..0b637326d4667 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -68,6 +68,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -90,6 +102,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); break; default: @@ -167,6 +189,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 1, }, + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -189,6 +223,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); break; default: @@ -232,6 +276,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'superuser at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql([ + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -254,6 +310,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index d7ec2e78ccb30..8e002bcc8d3da 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -11,6 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { describe('Actions', () => { loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); + loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 6eed28cc381dd..d8e4f808f5cd2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -165,6 +165,100 @@ instanceStateValue: true } }); + it('should schedule task, run alert and schedule preconfigured actions when appropriate', async () => { + const testStart = new Date(); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + indexRecordActionId: 'preconfigured.test.index-record', + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + + // Wait for the action to index a document before disabling the alert and waiting for tasks to finish + await esTestIndexTool.waitForDocs('action:test.index-record', reference); + + await taskManagerUtils.waitForAllTasksIdle(testStart); + + const alertId = response.body.id; + await alertUtils.disable(alertId); + await taskManagerUtils.waitForEmpty(testStart); + + // Ensure only 1 alert executed with proper params + const alertSearchResult = await esTestIndexTool.search( + 'alert:test.always-firing', + reference + ); + expect(alertSearchResult.hits.total.value).to.eql(1); + expect(alertSearchResult.hits.hits[0]._source).to.eql({ + source: 'alert:test.always-firing', + reference, + state: {}, + params: { + index: ES_TEST_INDEX_NAME, + reference, + }, + alertInfo: { + alertId, + spaceId: space.id, + namespace: space.id, + name: 'abc', + tags: ['tag-A', 'tag-B'], + createdBy: user.fullName, + updatedBy: user.fullName, + }, + }); + + // Ensure only 1 action executed with proper params + const actionSearchResult = await esTestIndexTool.search( + 'action:test.index-record', + reference + ); + expect(actionSearchResult.hits.total.value).to.eql(1); + expect(actionSearchResult.hits.hits[0]._source).to.eql({ + config: { + unencrypted: 'ignored-but-required', + }, + secrets: { + encrypted: 'this-is-also-ignored-and-also-required', + }, + params: { + index: ES_TEST_INDEX_NAME, + reference, + message: ` +alertId: ${alertId}, +alertName: abc, +spaceId: ${space.id}, +tags: tag-A,tag-B, +alertInstanceId: 1, +instanceContextValue: true, +instanceStateValue: true +`.trim(), + }, + reference, + source: 'action:test.index-record', + }); + + await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should pass updated alert params to executor', async () => { const testStart = new Date(); // create an alert diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index 517c64f178af5..ec59e56b08308 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -45,6 +45,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -67,6 +79,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); }); @@ -88,6 +110,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/action/_getAll`).expect(200, [ + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -110,6 +144,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); }); }); diff --git a/x-pack/test/functional/apps/discover/preserve_url.ts b/x-pack/test/functional/apps/discover/preserve_url.ts index 85142336a0a6b..9b9b2e2c60840 100644 --- a/x-pack/test/functional/apps/discover/preserve_url.ts +++ b/x-pack/test/functional/apps/discover/preserve_url.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'discover', 'spaceSelector', 'header']); - const appsMenu = getService('appsMenu'); const globalNav = getService('globalNav'); describe('preserve url', function() { @@ -26,8 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.saveSearch('A Search'); await PageObjects.common.navigateToApp('home'); - await appsMenu.clickLink('Discover'); - await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.header.clickDiscover(); const activeTitle = await globalNav.getLastBreadcrumb(); expect(activeTitle).to.be('A Search'); }); @@ -42,7 +40,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.spaceSelector.expectHomePage('another-space'); // other space - await appsMenu.clickLink('Discover'); + await PageObjects.header.clickDiscover(); await PageObjects.discover.saveSearch('A Search in another space'); await PageObjects.spaceSelector.openSpacesNav(); @@ -50,7 +48,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.spaceSelector.expectHomePage('default'); // default space - await appsMenu.clickLink('Discover'); + await PageObjects.header.clickDiscover(); await PageObjects.discover.waitUntilSearchingHasFinished(); const activeTitleDefaultSpace = await globalNav.getLastBreadcrumb(); expect(activeTitleDefaultSpace).to.be('A Search'); @@ -60,7 +58,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.spaceSelector.expectHomePage('another-space'); // other space - await appsMenu.clickLink('Discover'); + await PageObjects.header.clickDiscover(); await PageObjects.discover.waitUntilSearchingHasFinished(); const activeTitleOtherSpace = await globalNav.getLastBreadcrumb(); expect(activeTitleOtherSpace).to.be('A Search in another space'); diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz index 2b204d0bde271..454d260a518cd 100644 Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json index cf647f5c53212..fbcfa4cbe49b3 100644 --- a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json @@ -9,45 +9,62 @@ "mappings": { "_meta": { "migrationMappingPropertyHashes": { - "action": "c0c235fba02ebd2a2412bcda79009b58", + "action": "6e96ac5e648f57523879661ea72525b7", "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "d49f9b8d1277c6004506eec20dc0b108", + "agent_actions": "ed270b46812f0fa1439366c428a2cf17", + "agent_configs": "38abaf89513877745c359e7700c0c66a", + "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "agents": "c3eeb7b9d97176f15f6d126370ab23c7", + "alert": "7b44fba6773e37c806ce290ea9b7024e", "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", "canvas-element": "7390014e1091044523666d97247392fc", "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", + "cases": "08b8b110dbca273d37e8aef131ecab61", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", "dashboard": "d00f614b29a80360e1190193fd333bab", + "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", + "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "epm-package": "0be91c6758421dd5d0f1a58e9e5bc7c3", "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", "index-pattern": "66eccb05066c5a89924f48a9e9736499", "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "84b320fd67209906333ffce261128462", + "inventory-view": "9ecce5b58867403613d82fe496470b34", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "lens": "21c3ea0763beb1ecb0162529706b88c5", "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", "namespace": "2f4316de49999235636386fe51dc06c1", + "outputs": "aee9782e0d500b867859650a36280165", "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", "references": "7997cf5a56cc02bdc9c93361bde732b0", "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", "search": "181661168bbadd1eff5902361e2a0d5c", "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-ui-timeline": "6485ab095be8d15246667b98a1a34295", + "siem-detection-engine-rule-actions": "90eee2e4635260f4be0a1da8f5bc0aa0", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", + "telemetry": "36a616f7026dfa617d6655df850fe16d", "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", "type": "2f4316de49999235636386fe51dc06c1", "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", "url": "c7f66a0df8b1b52f17c28c4adb111105", "visualization": "52d7a13ad68a150c4525b292d23e12cc" } @@ -64,6 +81,11 @@ "type": "object" }, "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, "type": "text" }, "secrets": { @@ -85,6 +107,145 @@ } } }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, "alert": { "properties": { "actions": { @@ -114,15 +275,18 @@ "apiKeyOwner": { "type": "keyword" }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, "createdBy": { "type": "keyword" }, "enabled": { "type": "boolean" }, - "interval": { - "type": "keyword" - }, "muteAll": { "type": "boolean" }, @@ -130,12 +294,24 @@ "type": "keyword" }, "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, "type": "text" }, "params": { "enabled": false, "type": "object" }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, "scheduledTaskId": { "type": "keyword" }, @@ -221,52 +397,947 @@ }, "apm-telemetry": { "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { + "agents": { "properties": { "dotnet": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "go": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "java": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "js-base": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "nodejs": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "python": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "ruby": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "rum-js": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } } } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" }, "@timestamp": { "type": "date" @@ -285,26 +1356,257 @@ "keyword": { "type": "keyword" } - }, - "type": "text" + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } } } }, - "canvas-workpad": { - "dynamic": "false", + "cases-user-actions": { "properties": { - "@created": { - "type": "date" + "action": { + "type": "keyword" }, - "@timestamp": { + "action_at": { "type": "date" }, - "name": { - "fields": { - "keyword": { + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { "type": "keyword" } - }, + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { "type": "text" } } @@ -389,6 +1691,136 @@ } } }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, "file-upload-telemetry": { "properties": { "filesUploadedTotalCount": { @@ -538,6 +1970,26 @@ } } }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, "customOptions": { "properties": { "field": { @@ -572,6 +2024,18 @@ }, "metric": { "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, "type": { "type": "keyword" } @@ -699,9 +2163,19 @@ } } }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, "mapsTotalCount": { "type": "long" }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, "timeCaptured": { "type": "date" } @@ -803,7 +2277,7 @@ }, "type": "text" }, - "space": { + "visualization": { "fields": { "keyword": { "ignore_above": 256, @@ -828,6 +2302,37 @@ "namespace": { "type": "keyword" }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, "query": { "properties": { "description": { @@ -917,6 +2422,73 @@ } } }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "dynamic": "true", + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, "siem-ui-timeline": { "properties": { "columns": { @@ -1051,6 +2623,9 @@ "description": { "type": "text" }, + "eventType": { + "type": "keyword" + }, "favorite": { "properties": { "favoriteDate": { @@ -1255,6 +2830,9 @@ }, "telemetry": { "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, "enabled": { "type": "boolean" }, @@ -1262,11 +2840,15 @@ "type": "date" }, "lastVersionChecked": { - "ignore_above": 256, + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { "type": "keyword" }, "sendUsageFrom": { - "ignore_above": 256, "type": "keyword" }, "userHasSeenNotice": { @@ -1315,6 +2897,13 @@ } } }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, "type": { "type": "keyword" }, @@ -1391,6 +2980,13 @@ } } }, + "uptime-dynamic-settings": { + "properties": { + "heartbeatIndices": { + "type": "keyword" + } + } + }, "url": { "properties": { "accessCount": { diff --git a/x-pack/test/functional/es_archives/reporting/historic/data.json.gz b/x-pack/test/functional/es_archives/reporting/historic/data.json.gz deleted file mode 100644 index ecb85ec6faca4..0000000000000 Binary files a/x-pack/test/functional/es_archives/reporting/historic/data.json.gz and /dev/null differ diff --git a/x-pack/test/functional/es_archives/reporting/historic/mappings.json b/x-pack/test/functional/es_archives/reporting/historic/mappings.json deleted file mode 100644 index 3a5af0158ce00..0000000000000 --- a/x-pack/test/functional/es_archives/reporting/historic/mappings.json +++ /dev/null @@ -1,386 +0,0 @@ -{ - "type": "index", - "value": { - "index": ".kibana", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "xPackMonitoring:showBanner": { - "type": "boolean" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "spaceId": { - "type": "keyword" - }, - "timelion-sheet": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - -{ - "type": "index", - "value": { - "index": ".reporting-2018.03.11", - "mappings": { - "properties": { - "attempts": { - "type": "short" - }, - "completed_at": { - "type": "date" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "jobtype": { - "type": "keyword" - }, - "max_attempts": { - "type": "short" - }, - "output": { - "properties": { - "content": { - "enabled": false, - "type": "object" - }, - "content_type": { - "type": "keyword" - }, - "max_size_reached": { - "type": "boolean" - } - } - }, - "payload": { - "enabled": false, - "type": "object" - }, - "priority": { - "type": "byte" - }, - "process_expiration": { - "type": "date" - }, - "started_at": { - "type": "date" - }, - "status": { - "type": "keyword" - }, - "timeout": { - "type": "long" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js index 7cdd1c083239b..cdfafeec1bf46 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.js @@ -19,24 +19,10 @@ export function ReportingPageProvider({ getService, getPageObjects }) { const log = getService('log'); const config = getService('config'); const testSubjects = getService('testSubjects'); - const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'security', 'settings', 'share', 'timePicker']); + const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']); class ReportingPage { - async initTests() { - log.debug('ReportingPage:initTests'); - await PageObjects.settings.navigateTo(); - await esArchiver.loadIfNeeded('../../functional/es_archives/logstash_functional'); - await esArchiver.load('reporting/historic'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - }); - - await browser.setWindowSize(1600, 850); - } - async forceSharedItemsContainerSize({ width }) { await browser.execute(` var el = document.querySelector('[data-shared-items-container]'); @@ -130,6 +116,20 @@ export function ReportingPageProvider({ getService, getPageObjects }) { return await retry.try(async () => await testSubjects.find('generateReportButton')); } + async isGenerateReportButtonDisabled() { + const generateReportButton = await this.getGenerateReportButton(); + return await retry.try(async () => { + const isDisabled = await generateReportButton.getAttribute('disabled'); + return isDisabled; + }); + } + + async canReportBeCreated() { + await this.clickGenerateReportButton(); + const success = await this.checkForReportingToasts(); + return success; + } + async checkUsePrintLayout() { // The print layout checkbox slides in as part of an animation, and tests can // attempt to click it too quickly, leading to flaky tests. The 500ms wait allows diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index c94e7116c5cea..bce8e08cd16d3 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -17,6 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const supertest = getService('supertest'); const find = getService('find'); + const retry = getService('retry'); async function createAlert(overwrites: Record = {}) { const { body: createdAlert } = await supertest @@ -38,8 +39,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return createdAlert; } - // FLAKY: https://github.com/elastic/kibana/issues/62472 - describe.skip('alerts', function() { + describe('alerts', function() { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('alertsTab'); @@ -48,10 +48,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); await pageObjects.triggersActionsUI.clickCreateAlertButton(); - const nameInput = await testSubjects.find('alertNameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(alertName); + await testSubjects.setValue('alertNameInput', alertName); await testSubjects.click('.index-threshold-SelectOption'); await testSubjects.click('selectIndexExpression'); const comboBox = await find.byCssSelector('#indexSelectSearchBox'); @@ -60,28 +57,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); await filterSelectItem.click(); await testSubjects.click('thresholdAlertTimeFieldSelect'); - const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); - await fieldOptions[1].click(); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); await nameInput.click(); - // test for normal connector - await testSubjects.click('.webhook-ActionTypeSelectOption'); - const webhookBodyInput = await find.byCssSelector('.ace_text-input'); - await webhookBodyInput.focus(); - await webhookBodyInput.type('{\\"test\\":1}'); - - await testSubjects.click('addAlertActionButton'); - // pre-configured connector is loaded an displayed correctly await testSubjects.click('.slack-ActionTypeSelectOption'); - expect(await (await find.byCssSelector('#my-slack1')).isDisplayed()).to.be(true); - const loggingMessageInput = await testSubjects.find('slackMessageTextArea'); - await loggingMessageInput.click(); - await loggingMessageInput.clearValue(); - await loggingMessageInput.type('test message'); + await testSubjects.click('createActionConnectorButton'); + const slackConnectorName = generateUniqueKey(); + await testSubjects.setValue('nameInput', slackConnectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); + const createdConnectorToastTitle = await pageObjects.common.closeToast(); + expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); + await testSubjects.setValue('slackMessageTextArea', 'test message'); await testSubjects.click('messageAddVariableButton'); - const variableMenuButton = await testSubjects.find('variableMenuButton-0'); - await variableMenuButton.click(); + await testSubjects.click('variableMenuButton-0'); + await testSubjects.click('saveAlertButton'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Saved '${alertName}'`); @@ -132,7 +129,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should edit an alert', async () => { const createdAlert = await createAlert({ alertTypeId: '.index-threshold', - name: 'new alert', + name: generateUniqueKey(), params: { aggType: 'count', termSize: 5, @@ -160,11 +157,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const editLink = await testSubjects.findAll('alertsTableCell-editLink'); await editLink[0].click(); - const updatedAlertName = 'Changed Alert Name'; - const nameInputToUpdate = await testSubjects.find('alertNameInput'); - await nameInputToUpdate.click(); - await nameInputToUpdate.clearValue(); - await nameInputToUpdate.type(updatedAlertName); + const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; + await testSubjects.setValue('alertNameInput', updatedAlertName, { clearWithKeyboard: true }); await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); @@ -217,10 +211,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const editLink = await testSubjects.findAll('alertsTableCell-editLink'); await editLink[0].click(); - const throttleInputToSetInitialValue = await testSubjects.find('throttleInput'); - await throttleInputToSetInitialValue.click(); - await throttleInputToSetInitialValue.clearValue(); - await throttleInputToSetInitialValue.type('1'); + await testSubjects.setValue('throttleInput', '1', { clearWithKeyboard: true }); await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); @@ -308,11 +299,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const editLink = await testSubjects.findAll('alertsTableCell-editLink'); await editLink[0].click(); - const updatedAlertName = 'Changed Alert Name'; - const nameInputToUpdate = await testSubjects.find('alertNameInput'); - await nameInputToUpdate.click(); - await nameInputToUpdate.clearValue(); - await nameInputToUpdate.type(updatedAlertName); + const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; + await testSubjects.setValue('alertNameInput', updatedAlertName); await testSubjects.click('cancelSaveEditedAlertButton'); await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); diff --git a/x-pack/test/reporting/.gitignore b/x-pack/test/reporting/.gitignore new file mode 100644 index 0000000000000..99ee4c44686a0 --- /dev/null +++ b/x-pack/test/reporting/.gitignore @@ -0,0 +1 @@ +functional/reports/session/ diff --git a/x-pack/test/reporting/configs/chromium_functional.js b/x-pack/test/reporting/configs/chromium_functional.js index 05c3b6c142946..753d2b2a20ab9 100644 --- a/x-pack/test/reporting/configs/chromium_functional.js +++ b/x-pack/test/reporting/configs/chromium_functional.js @@ -5,6 +5,7 @@ */ export default async function({ readConfigFile }) { + // TODO move reporting tests to x-pack/test/functional/apps//reporting const functionalConfig = await readConfigFile(require.resolve('../../functional/config.js')); return { diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 012f0922c28cf..6107363986a40 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -15,8 +15,14 @@ const mkdirAsync = promisify(fs.mkdir); const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); +/* + * TODO Remove this file and spread the tests to various apps + */ + export default function({ getService, getPageObjects }) { - const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); const config = getService('config'); const PageObjects = getPageObjects([ 'reporting', @@ -27,65 +33,32 @@ export default function({ getService, getPageObjects }) { 'visualize', 'visEditor', ]); - const log = getService('log'); describe('Reporting', () => { - before('initialize tests', async () => { - await PageObjects.reporting.initTests(); - }); - - const expectDisabledGenerateReportButton = async () => { - const generateReportButton = await PageObjects.reporting.getGenerateReportButton(); - await retry.try(async () => { - const isDisabled = await generateReportButton.getAttribute('disabled'); - expect(isDisabled).to.be('true'); + describe('Dashboard', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); }); - }; - - const expectEnabledGenerateReportButton = async () => { - const generateReportButton = await PageObjects.reporting.getGenerateReportButton(); - await retry.try(async () => { - const isDisabled = await generateReportButton.getAttribute('disabled'); - expect(isDisabled).to.be(null); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); }); - }; - - const expectReportCanBeCreated = async () => { - await PageObjects.reporting.clickGenerateReportButton(); - const success = await PageObjects.reporting.checkForReportingToasts(); - expect(success).to.be(true); - }; - - const writeSessionReport = async (name, rawPdf, reportExt = 'pdf') => { - const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); - await mkdirAsync(sessionDirectory, { recursive: true }); - const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); - await writeFileAsync(sessionReportPath, rawPdf); - return sessionReportPath; - }; - - const getBaselineReportPath = (fileName, reportExt = 'pdf') => { - const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); - const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); - log.debug(`getBaselineReportPath (${fullPath})`); - return fullPath; - }; - - describe('Dashboard', () => { - beforeEach(() => PageObjects.reporting.clearToastNotifications()); describe('Print PDF button', () => { it('is not available if new', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.reporting.openPdfReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); await PageObjects.reporting.openPdfReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); }); @@ -95,15 +68,7 @@ export default function({ getService, getPageObjects }) { // function is taking about 15 seconds per comparison in jenkins. this.timeout(300000); await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('My PDF Dashboard'); - await PageObjects.reporting.setTimepickerInDataRange(); - const visualizations = PageObjects.dashboard.getTestVisualizationNames(); - - const tileMapIndex = visualizations.indexOf('Visualization TileMap'); - visualizations.splice(tileMapIndex, 1); - - await PageObjects.dashboard.addVisualizations(visualizations); - await PageObjects.dashboard.saveDashboard('report test'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); await PageObjects.reporting.openPdfReportingPanel(); await PageObjects.reporting.checkUsePrintLayout(); await PageObjects.reporting.clickGenerateReportButton(); @@ -121,30 +86,36 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.reporting.openPngReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { await PageObjects.dashboard.saveDashboard('My PNG Dash'); await PageObjects.reporting.openPngReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); }); describe('Preserve Layout', () => { it('matches baseline report', async function() { + const writeSessionReport = async (name, rawPdf, reportExt) => { + const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); + await mkdirAsync(sessionDirectory, { recursive: true }); + const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); + await writeFileAsync(sessionReportPath, rawPdf); + return sessionReportPath; + }; + const getBaselineReportPath = (fileName, reportExt) => { + const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); + const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); + log.debug(`getBaselineReportPath (${fullPath})`); + return fullPath; + }; + this.timeout(300000); await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('My PNG Dash'); - await PageObjects.reporting.setTimepickerInDataRange(); - - const visualizations = PageObjects.dashboard.getTestVisualizationNames(); - const tileMapIndex = visualizations.indexOf('Visualization TileMap'); - visualizations.splice(tileMapIndex, 1); - - await PageObjects.dashboard.addVisualizations(visualizations); - await PageObjects.dashboard.saveDashboard('PNG report test'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); await PageObjects.reporting.openPngReportingPanel(); await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); await PageObjects.reporting.clickGenerateReportButton(); @@ -167,44 +138,64 @@ export default function({ getService, getPageObjects }) { }); describe('Discover', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + }); + describe('Generate CSV button', () => { beforeEach(() => PageObjects.common.navigateToApp('discover')); it('is not available if new', async () => { await PageObjects.reporting.openCsvReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton'); await PageObjects.reporting.openCsvReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); it('generates a report with data', async () => { await PageObjects.reporting.setTimepickerInDataRange(); await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - await expectReportCanBeCreated(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); }); it('generates a report with no data', async () => { await PageObjects.reporting.setTimepickerInNoDataRange(); await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - await expectReportCanBeCreated(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); }); }); }); describe('Visualize', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + describe('Print PDF button', () => { it('is not available if new', async () => { await PageObjects.common.navigateToUrl('visualize', 'new'); await PageObjects.visualize.clickAreaChart(); - await PageObjects.visualize.clickNewSearch(); + await PageObjects.visualize.clickNewSearch('ecommerce'); await PageObjects.reporting.openPdfReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { @@ -214,14 +205,16 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.clickGo(); await PageObjects.visualize.saveVisualization('my viz'); await PageObjects.reporting.openPdfReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); - it('matches baseline report', async function() { + it('downloaded PDF has OK status', async function() { // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs // function is taking about 15 seconds per comparison in jenkins. this.timeout(180000); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); await PageObjects.reporting.openPdfReportingPanel(); await PageObjects.reporting.clickGenerateReportButton(); diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.pdf b/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.pdf deleted file mode 100644 index b7370109c687c..0000000000000 Binary files a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.pdf and /dev/null differ diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png b/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png index a0dfea9ef4fa7..1eb5f29d212c2 100644 Binary files a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png and b/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png differ diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_print.pdf b/x-pack/test/reporting/functional/reports/baseline/dashboard_print.pdf deleted file mode 100644 index bf2bca54ca2d7..0000000000000 Binary files a/x-pack/test/reporting/functional/reports/baseline/dashboard_print.pdf and /dev/null differ diff --git a/x-pack/test/reporting/functional/reports/baseline/visualize_print.pdf b/x-pack/test/reporting/functional/reports/baseline/visualize_print.pdf deleted file mode 100644 index c11967c12ebf5..0000000000000 Binary files a/x-pack/test/reporting/functional/reports/baseline/visualize_print.pdf and /dev/null differ diff --git a/yarn.lock b/yarn.lock index fdfdcada62e2c..19d28aea1acfd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2557,6 +2557,11 @@ resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" @@ -3461,6 +3466,13 @@ "@svgr/plugin-svgo" "^4.2.0" loader-utils "^1.2.3" +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + "@testim/chrome-version@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.0.7.tgz#0cd915785ec4190f08a3a6acc9b61fc38fb5f1a9" @@ -5926,6 +5938,40 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +apidoc-core@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/apidoc-core/-/apidoc-core-0.11.1.tgz#b04a7e0292e4ac0d714b40789f1b92f414486c81" + integrity sha512-pt/ICBdFQCZTgL38Aw1XB3G9AajDU1JA5E3yoDEgg0mqbPTCkOL8AyWdysjvNtQS/kkXgSPazCZaZzZYqrPHog== + dependencies: + fs-extra "^8.1.0" + glob "^7.1.4" + iconv-lite "^0.5.0" + klaw-sync "^6.0.0" + lodash "~4.17.15" + semver "~6.3.0" + +apidoc-markdown@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/apidoc-markdown/-/apidoc-markdown-5.0.0.tgz#e2d59d7cbbaa10402b09cec3e8ec17a03a27be59" + integrity sha512-gp4I4MvtgJvZPikEd7lwn149jjnC454CanPhm5demROdHCuakY+3YtIKEgVrJOqnS2iwbeeF+u4riB9CoO11+A== + dependencies: + ejs "^3.0.1" + semver "^7.1.3" + yargs "^15.1.0" + +apidoc@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/apidoc/-/apidoc-0.20.1.tgz#b29a2e2ae47e2df6a29e1f1527b94edf91853072" + integrity sha512-V54vkZ2lDFBiGn0qusZmHbMi4svuFBq0rjZAIe3nwYvBY7iztW78vKOyHyTr9ASaTB7EGe8hhLbpEnYAIO31TQ== + dependencies: + apidoc-core "^0.11.1" + commander "^2.20.0" + fs-extra "^8.1.0" + lodash "^4.17.15" + markdown-it "^10.0.0" + nodemon "^2.0.2" + winston "^3.2.1" + apollo-cache-control@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" @@ -7660,6 +7706,20 @@ boxen@^3.0.0: type-fest "^0.3.0" widest-line "^2.0.0" +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -8129,6 +8189,19 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + cachedir@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -8635,6 +8708,21 @@ chokidar@^2.0.0, chokidar@^2.1.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.2.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" + integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.3.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.0.1, chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -8957,7 +9045,7 @@ clone-regexp@^1.0.0: is-regexp "^1.0.0" is-supported-regexp-flag "^1.0.0" -clone-response@1.0.2: +clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -9423,6 +9511,18 @@ configstore@^3.1.2: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + connect-history-api-fallback@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" @@ -10021,6 +10121,11 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + cson-parser@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-1.3.5.tgz#7ec675e039145533bf2a6a856073f1599d9c2d24" @@ -10858,6 +10963,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -11406,6 +11516,13 @@ dot-prop@^4.1.0, dot-prop@^4.1.1: dependencies: is-obj "^1.0.0" +dot-prop@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" + integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + dependencies: + is-obj "^2.0.0" + dotenv-defaults@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/dotenv-defaults/-/dotenv-defaults-1.0.2.tgz#441cf5f067653fca4bbdce9dd3b803f6f84c585d" @@ -11603,6 +11720,11 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== +ejs@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.0.2.tgz#745b01cdcfe38c1c6a2da3bbb2d9957060a31226" + integrity sha512-IncmUpn1yN84hy2shb0POJ80FWrfGNY0cxO9f4v+/sG7qcBvAtVWUA1IdzY/8EYUmOVhoKJVdJjNd3AZcnxOjA== + elastic-apm-http-client@^9.2.0: version "9.2.1" resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.2.1.tgz#e0e980ceb9975ff770bdbf2f5cdaac39fd70e8e6" @@ -12137,6 +12259,11 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.1" es6-symbol "^3.1.1" +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -13965,7 +14092,7 @@ fs-exists-sync@^0.1.0: resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= -fs-extra@8.1.0, fs-extra@^8.0.1: +fs-extra@8.1.0, fs-extra@^8.0.1, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -14062,7 +14189,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@~2.1.0, fsevents@~2.1.1: +fsevents@~2.1.0, fsevents@~2.1.1, fsevents@~2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== @@ -14294,7 +14421,7 @@ get-stream@^2.2.0: object-assign "^4.0.1" pinkie-promise "^2.0.0" -get-stream@^4.0.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== @@ -14548,6 +14675,13 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" +global-dirs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" + integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== + dependencies: + ini "^1.3.5" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -14868,6 +15002,23 @@ got@^8.3.1, got@^8.3.2: url-parse-lax "^3.0.0" url-to-options "^1.0.1" +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.4: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -15572,6 +15723,11 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + has@^1.0.1, has@^1.0.3, has@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -15898,6 +16054,11 @@ http-cache-semantics@3.8.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -16162,6 +16323,11 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + ignore@^3.1.2, ignore@^3.3.5: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" @@ -16977,6 +17143,14 @@ is-installed-globally@0.1.0, is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + is-integer@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" @@ -17047,6 +17221,11 @@ is-npm@^1.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" @@ -17079,6 +17258,11 @@ is-obj@^1.0.0, is-obj@^1.0.1: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + is-object@^1.0.1, is-object@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" @@ -17343,6 +17527,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + is2@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.1.tgz#8ac355644840921ce435d94f05d3a94634d3481a" @@ -18636,6 +18825,13 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -18672,6 +18868,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -18732,6 +18935,13 @@ latest-version@^3.0.0, latest-version@^3.1.0: dependencies: package-json "^4.0.0" +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + lazy-ass@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -19599,11 +19809,16 @@ lowercase-keys@1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= -lowercase-keys@^1.0.0: +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lowlight@~1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.1.tgz#ed7c3dffc36f8c1f263735c0fe0c907847c11250" @@ -20247,7 +20462,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0: +mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -21224,6 +21439,22 @@ nodemailer@^4.7.0: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.7.0.tgz#4420e06abfffd77d0618f184ea49047db84f4ad8" integrity sha512-IludxDypFpYw4xpzKdMAozBSkzKHmNBvGanUREjJItgJ2NYcK/s8+PggVhj7c2yGFQykKsnnmv1+Aqo0ZfjHmw== +nodemon@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.3.tgz#e9c64df8740ceaef1cb00e1f3da57c0a93ef3714" + integrity sha512-lLQLPS90Lqwc99IHe0U94rDgvjo+G9I4uEIxRG3evSLROcqQ9hwc0AxlSHKS4T1JW/IMj/7N5mthiN58NL/5kw== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.2" + update-notifier "^4.0.0" + "nomnom@>= 1.5.x": version "1.8.1" resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" @@ -21246,6 +21477,13 @@ nopt@^2.2.0: dependencies: abbrev "1" +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" @@ -21307,6 +21545,11 @@ normalize-url@^3.3.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + now-and-later@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee" @@ -21993,6 +22236,11 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -22175,6 +22423,16 @@ package-json@^5.0.0: registry-url "^3.1.0" semver "^5.5.0" +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + pad-component@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/pad-component/-/pad-component-0.0.1.tgz#ad1f22ce1bf0fdc0d6ddd908af17f351a404b8ac" @@ -22678,6 +22936,11 @@ picomatch@^2.0.4, picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== +picomatch@^2.0.7: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -23322,6 +23585,11 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== +pstree.remy@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3" + integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A== + public-encrypt@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" @@ -23509,6 +23777,13 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +pupa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" + integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== + dependencies: + escape-goat "^2.0.0" + puppeteer-core@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-1.19.0.tgz#3c3f98edb5862583e3a9c19cbc0da57ccc63ba5c" @@ -23766,7 +24041,7 @@ raw-loader@~0.5.1: resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -24850,6 +25125,13 @@ readdirp@~3.2.0: dependencies: picomatch "^2.0.4" +readdirp@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" + integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== + dependencies: + picomatch "^2.0.7" + readline2@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" @@ -25187,6 +25469,13 @@ registry-auth-token@^3.0.1, registry-auth-token@^3.3.2: rc "^1.1.6" safe-buffer "^5.0.1" +registry-auth-token@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" + integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA== + dependencies: + rc "^1.2.8" + registry-url@^3.0.0, registry-url@^3.0.3, registry-url@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" @@ -25194,6 +25483,13 @@ registry-url@^3.0.0, registry-url@^3.0.3, registry-url@^3.1.0: dependencies: rc "^1.0.1" +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + regjsgen@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" @@ -25793,7 +26089,7 @@ resolve@~1.10.1: dependencies: path-parse "^1.0.6" -responselike@1.0.2: +responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= @@ -26382,6 +26678,13 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + semver-greatest-satisfied-range@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" @@ -26431,7 +26734,7 @@ semver@^5.5.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@~6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -26441,6 +26744,11 @@ semver@^6.1.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b" integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ== +semver@^7.1.3: + version "7.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.0.tgz#91f7c70ec944a63e5dc7a74cde2da375d8e0853c" + integrity sha512-uyvgU/igkrMgNHwLgXvlpD9jEADbJhB0+JXSywoO47JgJ6c16iau9F9cjtc/E5o0PoqRYTiTIAPRKaYe84z6eQ== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -27594,7 +27902,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -28372,6 +28680,11 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +term-size@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" + integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== + terser-webpack-plugin@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" @@ -28766,6 +29079,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -28850,6 +29168,13 @@ topojson-client@3.0.0, topojson-client@^3.0.0: dependencies: commander "2" +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tough-cookie@>=2.3.3, tough-cookie@^2.0.0, tough-cookie@^2.3.3, tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -29728,6 +30053,13 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +undefsafe@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" + integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== + dependencies: + debug "^2.2.0" + underscore.string@~3.3.4: version "3.3.5" resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.5.tgz#fc2ad255b8bd309e239cbc5816fd23a9b7ea4023" @@ -29915,6 +30247,13 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + unist-util-is@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" @@ -30084,6 +30423,25 @@ update-notifier@^2.5.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" +update-notifier@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3" + integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + upper-case-first@^1.1.0, upper-case-first@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" @@ -31385,6 +31743,13 @@ widest-line@^2.0.1: dependencies: string-width "^2.1.1" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + win-release@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/win-release/-/win-release-1.1.1.tgz#5fa55e02be7ca934edfc12665632e849b72e5209" @@ -31682,6 +32047,11 @@ xdg-basedir@^3.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xhr@^2.0.1: version "2.4.1" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.4.1.tgz#ba982cced205ae5eec387169ac9dc77ca4853d38" @@ -32003,7 +32373,7 @@ yargs@^13.2.2, yargs@^13.3.0: y18n "^4.0.0" yargs-parser "^13.1.1" -yargs@^15.0.2, yargs@^15.3.1: +yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1: version "15.3.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==