diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 8a972c65f8412..3ca6b6a4b9098 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.15.3 +ARG NODE_VERSION=14.15.4 FROM node:${NODE_VERSION} AS base diff --git a/.ci/jobs.yml b/.ci/jobs.yml index d4ec8a3d5a699..f62ec9510d2d4 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -1,4 +1,4 @@ -# This file is needed by functionalTests:ensureAllTestsInCiGroup for the list of ciGroups. That must be changed before this file can be removed +# This file is needed by node scripts/ensure_all_tests_in_ci_group for the list of ciGroups. That must be changed before this file can be removed JOB: - kibana-intake diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index bc427bf927f11..bbdf5484faf65 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -5,6 +5,17 @@ set -e branch="$1" checkoutDir="$(pwd)" +function cleanup() +{ + if [[ "$branch" != "master" ]]; then + rm --preserve-root -rf "$checkoutDir" + fi + + exit 0 +} + +trap 'cleanup' 0 + if [[ "$branch" != "master" ]]; then checkoutDir="/tmp/kibana-$branch" git clone https://github.com/elastic/kibana.git --branch "$branch" --depth 1 "$checkoutDir" @@ -56,6 +67,3 @@ echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" .ci/build_docker.sh -if [[ "$branch" != "master" ]]; then - rm --preserve-root -rf "$checkoutDir" -fi diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 94afc5dd22ba9..cbdf292964472 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -138,6 +138,10 @@ /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis +/x-pack/plugins/stack_alerts/server/alert_types/geo_containment @elastic/kibana-gis +/x-pack/plugins/stack_alerts/public/alert_types/geo_containment @elastic/kibana-gis +/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold @elastic/kibana-gis +/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /src/plugins/maps_oss/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis diff --git a/.node-version b/.node-version index 19c4c189d3640..c91434ab584a7 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.15.3 +14.15.4 diff --git a/.nvmrc b/.nvmrc index 19c4c189d3640..c91434ab584a7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.15.3 +14.15.4 diff --git a/NOTICE.txt b/NOTICE.txt index bf3cb4aa4ac87..2341a478cbda9 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Kibana source code with Kibana X-Pack source code -Copyright 2012-2020 Elasticsearch B.V. +Copyright 2012-2021 Elasticsearch B.V. --- Pretty handling of logarithmic axes. diff --git a/dev_docs/api_welcome.mdx b/dev_docs/api_welcome.mdx new file mode 100644 index 0000000000000..692a1bb2e582e --- /dev/null +++ b/dev_docs/api_welcome.mdx @@ -0,0 +1,14 @@ +--- +id: kibDevDocsApiWelcome +slug: /kibana-dev-docs/api-welcome +title: Welcome +summary: The home of automatically generated plugin API documentation using extracted TSDocs +date: 2021-01-02 +tags: ['kibana','dev', 'contributor', 'api docs'] +--- + +Welcome to Kibana's plugin API documentation. As a plugin developer, this is where you can +learn the details of every service you can take advantage of to help you build awe-inspiring creative solutions and applications! + +If you have any questions or issues, please reach out to the Kibana platform team or create an issue [here](https://github.com/elastic/kibana/issues). + diff --git a/dev_docs/dev_welcome.mdx b/dev_docs/dev_welcome.mdx new file mode 100644 index 0000000000000..cc185e689fa43 --- /dev/null +++ b/dev_docs/dev_welcome.mdx @@ -0,0 +1,17 @@ +--- +id: kibDevDocsWelcome +slug: /kibana-dev-docs/welcome +title: Welcome +summary: Build custom solutions and applications on top of Kibana. +date: 2021-01-02 +tags: ['kibana','dev', 'contributor'] +--- + +Welcome to Kibana's plugin developer documentation! + +Did you know that the vast majority of functionality built inside of Kibana is a plugin? A handful of core services hold the system together, +but it's our vast system of plugin developers that provide the amazing, out of the box, functionality you can use when building your own set of +custom utilities and applications. + +Browse the `Services` section to view all the plugins that offer functionality you can take advantage of, or check out the +`API documentation` to dig into the nitty gritty details of every public plugin API. diff --git a/docs/apm/deployment-annotations.asciidoc b/docs/apm/deployment-annotations.asciidoc index 53fd963a81f73..8add9c58a4cab 100644 --- a/docs/apm/deployment-annotations.asciidoc +++ b/docs/apm/deployment-annotations.asciidoc @@ -6,6 +6,9 @@ Track deployments with annotations ++++ +[role="screenshot"] +image::apm/images/apm-transaction-annotation.png[Example view of transactions annotation in the APM app in Kibana] + For enhanced visibility into your deployments, we offer deployment annotations on all transaction charts. This feature enables you to easily determine if your deployment has increased response times for an end-user, or if the memory/CPU footprint of your application has changed. @@ -43,6 +46,3 @@ See the <> reference for more information. NOTE: If custom annotations have been created for the selected time period, any derived annotations, i.e., those created automatically when `service.version` changes, will not be shown. - -[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/errors.asciidoc b/docs/apm/errors.asciidoc index 49351ec255858..c468d7f0235b2 100644 --- a/docs/apm/errors.asciidoc +++ b/docs/apm/errors.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[errors]] -=== Errors overview +=== Errors TIP: {apm-overview-ref-v}/errors.html[Errors] are groups of exceptions with a similar exception or log message. diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index 89ce0be1499c5..c185fdb43faf1 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -6,26 +6,20 @@ Get started ++++ -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. - -Curated charts and tables display the different types of APM data, which allows you to compare and debug your applications easily. +For a quick, high-level overview of the health and performance of your application, +start with: * <> * <> +* <> + +Notice something awry? Select a service or trace and dive deeper with: + +* <> * <> * <> * <> * <> -* <> TIP: Want to learn more about the Elastic APM ecosystem? See the {apm-get-started-ref}/overview.html[APM Overview]. @@ -34,6 +28,10 @@ include::services.asciidoc[] include::traces.asciidoc[] +include::service-maps.asciidoc[] + +include::service-overview.asciidoc[] + include::transactions.asciidoc[] include::spans.asciidoc[] @@ -41,5 +39,3 @@ include::spans.asciidoc[] include::errors.asciidoc[] include::metrics.asciidoc[] - -include::service-maps.asciidoc[] diff --git a/docs/apm/images/apm-agent-configuration.png b/docs/apm/images/apm-agent-configuration.png index 05518cb924d1b..07398f0609187 100644 Binary files a/docs/apm/images/apm-agent-configuration.png and b/docs/apm/images/apm-agent-configuration.png differ diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png index c68b36f522bfc..2ac91b6b19219 100644 Binary files a/docs/apm/images/apm-alert.png and b/docs/apm/images/apm-alert.png differ diff --git a/docs/apm/images/apm-errors-overview.png b/docs/apm/images/apm-errors-overview.png index 969a1f19f9f43..5b3b00a3b1ef2 100644 Binary files a/docs/apm/images/apm-errors-overview.png and b/docs/apm/images/apm-errors-overview.png differ diff --git a/docs/apm/images/apm-geo-ui.png b/docs/apm/images/apm-geo-ui.png index 3757127bad9c0..5bbe713c908a4 100644 Binary files a/docs/apm/images/apm-geo-ui.png and b/docs/apm/images/apm-geo-ui.png differ diff --git a/docs/apm/images/apm-logs-tab.png b/docs/apm/images/apm-logs-tab.png new file mode 100644 index 0000000000000..77aecf744bc7f Binary files /dev/null and b/docs/apm/images/apm-logs-tab.png differ diff --git a/docs/apm/images/apm-metrics.png b/docs/apm/images/apm-metrics.png index ffe5ffc7e1d83..af083b5ba3c08 100644 Binary files a/docs/apm/images/apm-metrics.png and b/docs/apm/images/apm-metrics.png differ diff --git a/docs/apm/images/apm-query-bar.png b/docs/apm/images/apm-query-bar.png index 90955fb61016d..92398065c2545 100644 Binary files a/docs/apm/images/apm-query-bar.png and b/docs/apm/images/apm-query-bar.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 85d14cc7dfc6e..85b441d47f0c2 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-settings.png b/docs/apm/images/apm-settings.png index 14cf32877b720..c821b7fb76e79 100644 Binary files a/docs/apm/images/apm-settings.png and b/docs/apm/images/apm-settings.png differ diff --git a/docs/apm/images/apm-setup.png b/docs/apm/images/apm-setup.png index 3f5f7761427de..3410ebf69d846 100644 Binary files a/docs/apm/images/apm-setup.png and b/docs/apm/images/apm-setup.png differ diff --git a/docs/apm/images/apm-traces.png b/docs/apm/images/apm-traces.png index bf1f7e783bb11..97e801606c613 100644 Binary files a/docs/apm/images/apm-traces.png and b/docs/apm/images/apm-traces.png differ diff --git a/docs/apm/images/apm-transaction-annotation.png b/docs/apm/images/apm-transaction-annotation.png index 8913770517ff6..b9360db2ff474 100644 Binary files a/docs/apm/images/apm-transaction-annotation.png and b/docs/apm/images/apm-transaction-annotation.png differ diff --git a/docs/apm/images/apm-transaction-response-dist.png b/docs/apm/images/apm-transaction-response-dist.png index 1d268bbaac465..2f3e69f263a28 100644 Binary files a/docs/apm/images/apm-transaction-response-dist.png and b/docs/apm/images/apm-transaction-response-dist.png differ diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png index bfdb6a5abe65b..0e4bc5f3f878a 100644 Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index 53d7637b18647..80fca19ff96cc 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/global-filters.png b/docs/apm/images/global-filters.png index 034828148c22a..70ae50aea6057 100644 Binary files a/docs/apm/images/global-filters.png and b/docs/apm/images/global-filters.png differ diff --git a/docs/apm/images/jvm-metrics-overview.png b/docs/apm/images/jvm-metrics-overview.png index 586836c6cfe3e..4b882574e2b9a 100644 Binary files a/docs/apm/images/jvm-metrics-overview.png and b/docs/apm/images/jvm-metrics-overview.png differ diff --git a/docs/apm/images/jvm-metrics.png b/docs/apm/images/jvm-metrics.png index 52a1ca5bea8d8..70f7965b72df6 100644 Binary files a/docs/apm/images/jvm-metrics.png and b/docs/apm/images/jvm-metrics.png differ diff --git a/docs/apm/images/local-filter.png b/docs/apm/images/local-filter.png index 8657e39f430aa..edcaf8b6a609c 100644 Binary files a/docs/apm/images/local-filter.png and b/docs/apm/images/local-filter.png differ diff --git a/docs/apm/images/service-maps-java.png b/docs/apm/images/service-maps-java.png index b3726bdc00ab6..d7c0786e406d9 100644 Binary files a/docs/apm/images/service-maps-java.png and b/docs/apm/images/service-maps-java.png differ diff --git a/docs/apm/images/service-maps.png b/docs/apm/images/service-maps.png index 878a31adc69ca..190b7af3c560e 100644 Binary files a/docs/apm/images/service-maps.png and b/docs/apm/images/service-maps.png differ diff --git a/docs/apm/metrics.asciidoc b/docs/apm/metrics.asciidoc index e64cbc846960d..8adcf315d33d3 100644 --- a/docs/apm/metrics.asciidoc +++ b/docs/apm/metrics.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[metrics]] -=== Metrics overview +=== Metrics The *Metrics* overview provides agent-specific metrics, which lets you perform more in-depth root cause analysis investigations within the APM app. diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index d44c4ff6caa5c..7cc4da8a1fc1d 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -1,12 +1,12 @@ [role="xpack"] [[service-maps]] -=== Service maps +=== Service map 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. If enabled, service maps also integrate with machine learning--for real time health indicators based on anomaly detection scores. -All of these features can help you to quickly and visually assess the status and health of your services. +All of these features can help you quickly and visually assess your services' status and health. We currently surface two types of service maps: diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc new file mode 100644 index 0000000000000..970d4c61ed92e --- /dev/null +++ b/docs/apm/service-overview.asciidoc @@ -0,0 +1,5 @@ +[role="xpack"] +[[service-overview]] +=== Service overview + +Selecting a <> brings you to the *Service overview*. \ No newline at end of file diff --git a/docs/apm/services.asciidoc b/docs/apm/services.asciidoc index 2bf2e35c21cd8..90ebff3d8ad71 100644 --- a/docs/apm/services.asciidoc +++ b/docs/apm/services.asciidoc @@ -1,14 +1,14 @@ [role="xpack"] [[services]] -=== Services overview +=== Services -The *Services* overview page provides a quick, high-level overview of the health and general +*Service* inventory provides a quick, high-level overview of the health and general performance of all instrumented services. To help surface potential issues, services are sorted by their health status: **critical** > **warning** > **healthy** > **unknown**. -Health status is powered by machine learning and requires anomaly detection to be enabled. -Learn more in <>. +Health status is powered by <> +and requires anomaly detection to be enabled. [role="screenshot"] image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana] diff --git a/docs/apm/traces.asciidoc b/docs/apm/traces.asciidoc index 3bafebd733159..904be1c8e769d 100644 --- a/docs/apm/traces.asciidoc +++ b/docs/apm/traces.asciidoc @@ -1,20 +1,21 @@ [role="xpack"] [[traces]] -=== Traces overview +=== Traces 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. +*Traces* displays your application's entry transactions. +Transactions with the same name are grouped together and only shown once in this table. 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 <>. +If there's a particular endpoint you're worried about, select it to view its +<>. [role="screenshot"] image::apm/images/apm-traces.png[Example view of the Traces overview in APM app in Kibana] diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index fef98a86de1d0..3f624980e3937 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -1,20 +1,18 @@ [role="xpack"] [[transactions]] -=== Transaction overview +=== Transactions TIP: A {apm-overview-ref-v}/transactions.html[transaction] describes an event captured by an Elastic APM agent instrumenting a service. APM agents automatically collect performance metrics on HTTP requests, database queries, and much more. -Selecting a <> brings you to the *transactions* overview. - [role="screenshot"] image::apm/images/apm-transactions-overview.png[Example view of transactions table in the APM app in Kibana] -The *transaction duration*, *transactions per minute*, *transaction error rate*, and *time spent by span type* +The *Latency*, *transactions per minute*, *Error rate*, and *Average duration by span type* charts display information on all transactions associated with the selected service: -*Transaction duration*:: -Response times for this service, broken down into average, 95th, and 99th percentile. +*Latency*:: +Response times for the service. Options include average, 95th, and 99th percentile. If there's a weird spike that you'd like to investigate, you can simply zoom in on the graph - this will adjust the specific time range, and all of the data on the page will update accordingly. @@ -24,12 +22,12 @@ Visualize response codes: `2xx`, `3xx`, `4xx`, etc., and is useful for determining if you're serving more of one code than you typically do. Like in the Transaction duration graph, you can zoom in on anomalies to further investigate them. -*Transaction error rate*:: +*Error rate*:: Visualize the total number of transactions with errors divided by the total number of transactions. Any unexpected increases, decreases, or irregular patterns can be investigated further with the <>. -*Time spent by span type*:: +*Average duration by span type*:: Visualize where your application is spending most of its time. For example, is your app spending time in external calls, database processing, or application code execution? + @@ -39,8 +37,9 @@ This could be a sign that the agent does not have auto-instrumentation for whate + It's important to note that if you have asynchronous spans, the sum of all span times may exceed the duration of the transaction. +[discrete] [[transactions-table]] -==== Transactions table +=== Transactions table The *Transactions* table displays a list of _transaction groups_ for the selected service. In other words, this view groups all transactions of the same name together, @@ -63,8 +62,9 @@ For further details, including troubleshooting and custom implementation instruc refer to the documentation for each {apm-agents-ref}[APM Agent] you've implemented. ==== +[discrete] [[rum-transaction-overview]] -==== RUM Transaction overview +=== RUM Transaction overview The transaction overview page is customized for the JavaScript RUM Agent. Specifically, the page highlights *page load times* for your service: @@ -77,8 +77,9 @@ are available in the Observability User Experience tab. // To do // Add link to the Observability UE docs when complete +[discrete] [[transaction-details]] -==== Transaction details +=== Transaction details Selecting a transaction group will bring you to the *transaction* details. This page is visually similar to the transaction overview, but it shows data from all transactions within @@ -87,20 +88,32 @@ the selected transaction group. [role="screenshot"] image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution] -Up to ten sampled transactions are also displayed. -These sampled transactions are based on the _bucket_ selection in the *Transactions duration distribution* chart. -You can update the sampled transactions by selecting a new _bucket_. -The number of requests per bucket is displayed when hovering over the graph, -and the selected bucket is highlighted to stand out. +[[transaction-duration-distribution]] +==== Transactions duration distribution -The screenshot below shows a typical distribution, and indicates most of our requests were served quickly--awesome! +This chart plots all transaction durations for the given time period. +The screenshot below 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. [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] -When you select a bucket, -you're presented with up to ten trace samples. +Select a transaction duration _bucket_ to display up to ten trace samples. + +[[transaction-trace-sample]] +==== Trace sample + +Trace samples are based on the _bucket_ selection in the *Transactions duration distribution* chart; +update the samples by selecting a new _bucket_. +The number of requests per bucket is displayed when hovering over the graph, +and the selected bucket is highlighted to stand out. + +Each bucket presents up to ten trace samples in a *timeline*, trace sample *metadata*, +and any related *logs*. + +*Trace sample timeline* + Each sample has a trace timeline waterfall that shows how a typical request in that bucket executed. This waterfall is useful for understanding the parent/child hierarchy of transactions and spans, and ultimately determining _why_ a request was slow. @@ -112,7 +125,9 @@ image::apm/images/apm-transaction-sample.png[Example view of transactions sample NOTE: More information on timeline waterfalls is available in <>. -For a particular transaction sample, we can get even more information in the *metadata* tab: +*Trace sample metadata* + +Learn more about a trace sample in the *Metadata* tab: * Labels - Custom labels added by agents * HTTP request/response information @@ -123,7 +138,22 @@ For a particular transaction sample, we can get even more information in the *me * Agent information * URL * User - Requires additional configuration, but allows you to see which user experienced the current transaction. -* Custom - You can configure your agent to add custom contextual information on transactions. TIP: All of this data is stored in documents in Elasticsearch. This means you can select "Actions - View sample document" to see the actual Elasticsearch document under the discover tab. + +*Trace sample logs* + +The *Logs* tab displays logs related to the sampled trace. + +Logs provide detailed information about specific events, +and are crucial to successfully debugging slow or erroneous transactions. + +If you've correlated your application's logs and traces, you never have to search for relevant data; +it's all provided on this. Viewing log and trace data together allows you to quickly diagnose +and solve problems. + +[role="screenshot"] +image::apm/images/apm-logs-tab.png[APM logs tab] + +// To do: link to log correlation \ No newline at end of file diff --git a/docs/developer/advanced/index.asciidoc b/docs/developer/advanced/index.asciidoc index 5c53bedd95e72..ea1cc810e8ff6 100644 --- a/docs/developer/advanced/index.asciidoc +++ b/docs/developer/advanced/index.asciidoc @@ -4,9 +4,12 @@ * <> * <> * <> +* <> include::development-es-snapshots.asciidoc[leveloffset=+1] include::running-elasticsearch.asciidoc[leveloffset=+1] -include::development-basepath.asciidoc[leveloffset=+1] \ No newline at end of file +include::development-basepath.asciidoc[leveloffset=+1] + +include::upgrading-nodejs.asciidoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc new file mode 100644 index 0000000000000..a6fa57581772a --- /dev/null +++ b/docs/developer/advanced/upgrading-nodejs.asciidoc @@ -0,0 +1,76 @@ +[[upgrading-nodejs]] +== Upgrading Node.js + +{kib} requires a specific Node.js version to run. +When running {kib} from source, you must have this version installed locally. + +The required version of Node.js is listed in several different files throughout the {kib} source code. +Theses files must be updated when upgrading Node.js: + + - {kib-repo}blob/{branch}/.ci/Dockerfile[`.ci/Dockerfile`] - The version is specified in the `NODE_VERSION` constant. + This is used to pull the relevant image from https://hub.docker.com/_/node[Docker Hub]. + Note that Docker Hub can take 24+ hours to be updated with the new images after a new release of Node.js, so if you're upgrading Node.js in Kibana _just_ after the official Node.js release, you have to check if the new images are present on Docker Hub. + If they are not, and the update is urgent, you can skip this file and update it later once Docker Hub has been updated. + - {kib-repo}blob/{branch}/.node-version[`.node-version`] + - {kib-repo}blob/{branch}/.nvmrc[`.nvmrc`] + - {kib-repo}blob/{branch}/package.json[`package.json`] - The version is specified in the `engines.node` field. + +See PR {kib-repo}pull/86593[#86593] for an example of how the Node.js version has been upgraded previously. + +In the 6.8 branch, the `.ci/Dockerfile` file does not exist, so when upgrading Node.js in that branch, just skip that file. + +=== Backporting + +The following rules are not set in stone. +Use best judgement when backporting. + +Currently version 7.11 and newer run Node.js 14, while 7.10 and older run Node.js 10. +Hence, upgrades to either Node.js 14 or Node.js 10 shold be done as separate PRs. + +==== Node.js patch upgrades + +Typically, you want to backport Node.js *patch* upgrades to all supported release branches that run the same _major_ Node.js version: + + - If upgrading Node.js 14, and the current release is 7.11.1, the main PR should target `master` and be backported to `7.x` and `7.11`. + - If upgrading Node.js 10, the main PR should target `6.8` only. + +==== Node.js minor upgrades + +Typically, you want to backport Node.js *minor* upgrades to the next minor {kib} release branch that runs the same *major* Node.js version: + + - If upgrading Node.js 14, and the current release is 7.11.1, the main PR should target `master` and be backported to `7.x`, while leaving the `7.11` branch as-is. + - If upgrading Node.js 10, the main PR should target `6.8` only. + +=== Upgrading installed Node.js version + +The following instructions expect that https://github.com/nvm-sh/nvm[nvm] is used to manage locally installed Node.js versions. + +Run the following to install the new Node.js version. Replace `` with the desired Node.js version: + +[source,bash] +---- +nvm install +---- + +To get the same global npm modules installed with the new version of Node.js as is currently installed, use the `--reinstall-packages-from` command-line argument (optionally replace `14` with the desired source version): + +[source,bash] +---- +nvm install --reinstall-packages-from=14 +---- + +If needed, uninstall the old version of Node.js by running the following. Replace `` with the full version number of the version that should be uninstalled: + +[source,bash] +---- +nvm uninstall +---- + +Optionally, tell nvm to always use the "highest" installed Node.js 14 version. Replace `14` if a different major version is desired: + +[source,bash] +---- +nvm alias default 14 +---- + +Alternatively, include the full version number at the end to specify a specific default version. diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index 1f07850909565..c116dfa510bc9 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -20,7 +20,7 @@ cd kibana Install the version of Node.js listed in the `.node-version` file. This can be automated with tools such as -https://github.com/creationix/nvm[nvm], +https://github.com/nvm-sh/nvm[nvm], https://github.com/coreybutler/nvm-windows[nvm-windows] or https://github.com/wbyoung/avn[avn]. As we also include a `.nvmrc` file you can switch to the correct version when using nvm by running: diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index d4d2b229eeba7..c79e46c1d9173 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -168,6 +168,10 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s |Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. +|{kib-repo}blob/{branch}/src/plugins/runtime_fields/README.mdx[runtimeFields] +|The runtime fields plugin provides types and constants for OSS and xpack runtime field related code. + + |{kib-repo}blob/{branch}/src/plugins/saved_objects/README.md[savedObjects] |The savedObjects plugin exposes utilities to manipulate saved objects on the client side. @@ -483,8 +487,8 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. -|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields/README.md[runtimeFields] -|Welcome to the home of the runtime field editor and everything related to runtime fields! +|{kib-repo}blob/{branch}/x-pack/plugins/runtime_field_editor/README.md[runtimeFieldEditor] +|Welcome to the home of the runtime field editor! |{kib-repo}blob/{branch}/x-pack/plugins/saved_objects_tagging/README.md[savedObjectsTagging] diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index d73ed716e6b19..0e23064385a63 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -15,6 +15,7 @@ readonly links: { readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; + readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; @@ -44,6 +45,7 @@ readonly links: { readonly aggs: { readonly date_histogram: string; readonly date_range: string; + readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; @@ -73,6 +75,7 @@ readonly links: { readonly sum: string; readonly top_hits: string; }; + readonly runtimeFields: string; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; @@ -87,6 +90,7 @@ readonly links: { }; readonly addData: string; readonly kibana: string; + readonly elasticsearch: Record; readonly siem: { readonly guide: string; readonly gettingStarted: string; @@ -104,5 +108,13 @@ readonly links: { readonly ml: Record; readonly transforms: Record; readonly visualize: Record; + readonly apis: Record; + readonly observability: Record; + readonly alerting: Record; + readonly maps: Record; + readonly monitoring: Record; + readonly security: Record; + readonly watcher: Record; + readonly ccs: Record; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 7aa170eef9b50..3ad747a42f84e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Record<string, string>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Record<string, string>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 7f671d9edcd86..1db3bd31bbc9b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -114,7 +114,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsFindResponsePublic](./kibana-plugin-core-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | | [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | -| [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) | Represents a failure to import. | +| [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) | The response describing the result of an import. | | [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.id.md deleted file mode 100644 index 72b9c86348f2e..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [id](./kibana-plugin-core-public.savedobjectsimporterror.id.md) - -## SavedObjectsImportError.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md deleted file mode 100644 index e12396e9fa7b9..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) - -## SavedObjectsImportError interface - -Represents a failure to import. - -Signature: - -```typescript -export interface SavedObjectsImportError -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [error](./kibana-plugin-core-public.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | -| [id](./kibana-plugin-core-public.savedobjectsimporterror.id.md) | string | | -| [meta](./kibana-plugin-core-public.savedobjectsimporterror.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-public.savedobjectsimporterror.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | -| [title](./kibana-plugin-core-public.savedobjectsimporterror.title.md) | string | | -| [type](./kibana-plugin-core-public.savedobjectsimporterror.type.md) | string | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.type.md deleted file mode 100644 index fee537160a2ad..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [type](./kibana-plugin-core-public.savedobjectsimporterror.type.md) - -## SavedObjectsImportError.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.error.md similarity index 62% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.error.md index 201f56bf925d1..16628e83b8af9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.error.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [error](./kibana-plugin-core-public.savedobjectsimporterror.error.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [error](./kibana-plugin-core-public.savedobjectsimportfailure.error.md) -## SavedObjectsImportError.error property +## SavedObjectsImportFailure.error property Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.id.md new file mode 100644 index 0000000000000..2279241083241 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [id](./kibana-plugin-core-public.savedobjectsimportfailure.id.md) + +## SavedObjectsImportFailure.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md new file mode 100644 index 0000000000000..f9219c9037e0a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) + +## SavedObjectsImportFailure interface + +Represents a failure to import. + +Signature: + +```typescript +export interface SavedObjectsImportFailure +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-public.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-core-public.savedobjectsimportfailure.id.md) | string | | +| [meta](./kibana-plugin-core-public.savedobjectsimportfailure.meta.md) | {
title?: string;
icon?: string;
} | | +| [overwrite](./kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | +| [title](./kibana-plugin-core-public.savedobjectsimportfailure.title.md) | string | | +| [type](./kibana-plugin-core-public.savedobjectsimportfailure.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.meta.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.meta.md similarity index 51% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.meta.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.meta.md index 97bf3c4cff8eb..4ea9455098035 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.meta.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.meta.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [meta](./kibana-plugin-core-public.savedobjectsimporterror.meta.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [meta](./kibana-plugin-core-public.savedobjectsimportfailure.meta.md) -## SavedObjectsImportError.meta property +## SavedObjectsImportFailure.meta property Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.overwrite.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md similarity index 54% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.overwrite.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md index 69a8726b0588a..579a16697b406 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.overwrite.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [overwrite](./kibana-plugin-core-public.savedobjectsimporterror.overwrite.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [overwrite](./kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md) -## SavedObjectsImportError.overwrite property +## SavedObjectsImportFailure.overwrite property If `overwrite` is specified, an attempt was made to overwrite an existing object. diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.title.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.title.md similarity index 53% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.title.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.title.md index 95eeaaedf94c5..0024358bda030 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.title.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.title.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [title](./kibana-plugin-core-public.savedobjectsimporterror.title.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [title](./kibana-plugin-core-public.savedobjectsimportfailure.title.md) -## SavedObjectsImportError.title property +## SavedObjectsImportFailure.title property > Warning: This API is now obsolete. > diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.type.md new file mode 100644 index 0000000000000..68411093a92ce --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [type](./kibana-plugin-core-public.savedobjectsimportfailure.type.md) + +## SavedObjectsImportFailure.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md index 95c831420f3f3..073eac20b04ac 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md @@ -7,5 +7,5 @@ Signature: ```typescript -errors?: SavedObjectsImportError[]; +errors?: SavedObjectsImportFailure[]; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md index 0aba4d517e43a..2c0b691c9d66e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportResponse | Property | Type | Description | | --- | --- | --- | -| [errors](./kibana-plugin-core-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | +| [errors](./kibana-plugin-core-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure[] | | | [success](./kibana-plugin-core-public.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-public.savedobjectsimportresponse.successcount.md) | number | | | [successResults](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md b/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md index d0fd524f3234f..715f15ec812a3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md @@ -31,7 +31,19 @@ How to restrict some capabilities ```ts // my-plugin/server/plugin.ts public setup(core: CoreSetup, deps: {}) { - core.capabilities.registerSwitcher((request, capabilities) => { + core.capabilities.registerSwitcher((request, capabilities, useDefaultCapabilities) => { + // useDefaultCapabilities is a special case that switchers typically don't have to concern themselves with. + // The default capabilities are typically the ones you provide in your CapabilitiesProvider, but this flag + // gives each switcher an opportunity to change the default capabilities of other plugins' capabilities. + // For example, you may decide to flip another plugin's capability to false if today is Tuesday, + // but you wouldn't want to do this when we are requesting the default set of capabilities. + if (useDefaultCapabilities) { + return { + somePlugin: { + featureEnabledByDefault: true + } + } + } if(myPluginApi.shouldRestrictSomePluginBecauseOf(request)) { return { somePlugin: { diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilitiesswitcher.md b/docs/development/core/server/kibana-plugin-core-server.capabilitiesswitcher.md index 01aa3a32c9abb..e6a0a9a096671 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilitiesswitcher.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilitiesswitcher.md @@ -9,5 +9,5 @@ See [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) Signature: ```typescript -export declare type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities) => Partial | Promise>; +export declare type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities, useDefaultCapabilities: boolean) => Partial | Promise>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.exportsavedobjectstostream.md b/docs/development/core/server/kibana-plugin-core-server.exportsavedobjectstostream.md deleted file mode 100644 index f8b5eb3b35393..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.exportsavedobjectstostream.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [exportSavedObjectsToStream](./kibana-plugin-core-server.exportsavedobjectstostream.md) - -## exportSavedObjectsToStream() function - -Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. - -Signature: - -```typescript -export declare function exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| { types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, } | SavedObjectsExportOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md deleted file mode 100644 index cebebbaf94fe6..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [importSavedObjectsFromStream](./kibana-plugin-core-server.importsavedobjectsfromstream.md) - -## importSavedObjectsFromStream() function - -Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. - -Signature: - -```typescript -export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| { readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsImportOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsexporter.md new file mode 100644 index 0000000000000..5c7385ea663d3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsexporter.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ISavedObjectsExporter](./kibana-plugin-core-server.isavedobjectsexporter.md) + +## ISavedObjectsExporter type + + +Signature: + +```typescript +export declare type ISavedObjectsExporter = PublicMethodsOf; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectsimporter.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsimporter.md new file mode 100644 index 0000000000000..b6bfe8de31895 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsimporter.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ISavedObjectsImporter](./kibana-plugin-core-server.isavedobjectsimporter.md) + +## ISavedObjectsImporter type + + +Signature: + +```typescript +export declare type ISavedObjectsImporter = PublicMethodsOf; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 269db90c4db9b..36da1b51ee7b0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -26,6 +26,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidationError](./kibana-plugin-core-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | +| [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) | | +| [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) | | +| [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) | | +| [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | | | [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | | [SavedObjectsSerializer](./kibana-plugin-core-server.savedobjectsserializer.md) | A serializer that can be used to manually convert [raw](./kibana-plugin-core-server.savedobjectsrawdoc.md) or [sanitized](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) documents to the other kind. | | [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) | | @@ -38,14 +42,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResultType](./kibana-plugin-core-server.authresulttype.md) | | | [AuthStatus](./kibana-plugin-core-server.authstatus.md) | Status indicating an outcome of the authentication. | -## Functions - -| Function | Description | -| --- | --- | -| [exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. | -| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | -| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | - ## Interfaces | Interface | Description | @@ -140,6 +136,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidatorOptions](./kibana-plugin-core-server.routevalidatoroptions.md) | Additional options for the RouteValidator class to modify its default behaviour. | | [SavedObject](./kibana-plugin-core-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-core-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | +| [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) | | | [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) | | [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. | @@ -163,7 +160,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | | [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | -| [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) | Options controlling the export operation. | +| [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | +| [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | | [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md) | | @@ -171,7 +169,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | | | [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | -| [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | Represents a failure to import. | +| [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) | Options to control the import operation. | | [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) | The response describing the result of an import. | @@ -249,6 +247,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ILegacyCustomClusterClient](./kibana-plugin-core-server.ilegacycustomclusterclient.md) | Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). | | [ILegacyScopedClusterClient](./kibana-plugin-core-server.ilegacyscopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md). | | [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) | Returns authentication status for a request. | +| [ISavedObjectsExporter](./kibana-plugin-core-server.isavedobjectsexporter.md) | | +| [ISavedObjectsImporter](./kibana-plugin-core-server.isavedobjectsimporter.md) | | | [ISavedObjectsRepository](./kibana-plugin-core-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | [ISavedObjectTypeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) | See [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) for documentation. | | [KibanaRequestRouteOptions](./kibana-plugin-core-server.kibanarequestrouteoptions.md) | Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index b195e97989162..3a5e84ffdc372 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -11,6 +11,8 @@ core: { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; + exporter: ISavedObjectsExporter; + importer: ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 1de7313f2c40e..5300c85cf9406 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exporter: ISavedObjectsExporter;
importer: ISavedObjectsImporter;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md deleted file mode 100644 index a2255613e0f6c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [resolveSavedObjectsImportErrors](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) - -## resolveSavedObjectsImportErrors() function - -Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. - -Signature: - -```typescript -export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, }: SavedObjectsResolveImportErrorsOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, } | SavedObjectsResolveImportErrorsOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md similarity index 54% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md index cd2f9815c631d..0972d82987f51 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [excludeExportDetails](./kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) -## SavedObjectsExportOptions.excludeExportDetails property +## SavedObjectExportBaseOptions.excludeExportDetails property flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md new file mode 100644 index 0000000000000..6a7c86c1af860 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) + +## SavedObjectExportBaseOptions.includeReferencesDeep property + +flag to also include all related saved objects in the export stream. + +Signature: + +```typescript +includeReferencesDeep?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md new file mode 100644 index 0000000000000..eb35bb6a4ea5c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) + +## SavedObjectExportBaseOptions interface + + +Signature: + +```typescript +export interface SavedObjectExportBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | +| [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | +| [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.namespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md similarity index 52% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.namespace.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md index 0a0d684da2e42..9a8dad24ac18e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.namespace.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [namespace](./kibana-plugin-core-server.savedobjectsexportoptions.namespace.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) -## SavedObjectsExportOptions.namespace property +## SavedObjectExportBaseOptions.namespace property optional namespace to override the namespace used by the savedObjectsClient. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md new file mode 100644 index 0000000000000..cb20fc5400125 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) + +## SavedObjectsExportByObjectOptions interface + +Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) + +Signature: + +```typescript +export interface SavedObjectsExportByObjectOptions extends SavedObjectExportBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md) | Array<{
id: string;
type: string;
}> | optional array of objects to export. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md new file mode 100644 index 0000000000000..a821ffee153be --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) > [objects](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md) + +## SavedObjectsExportByObjectOptions.objects property + +optional array of objects to export. + +Signature: + +```typescript +objects: Array<{ + id: string; + type: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md new file mode 100644 index 0000000000000..a58818e27328a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) > [hasReference](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md) + +## SavedObjectsExportByTypeOptions.hasReference property + +optional array of references to search object for. + +Signature: + +```typescript +hasReference?: SavedObjectsFindOptionsReference[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md new file mode 100644 index 0000000000000..26ebfd658f19b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) + +## SavedObjectsExportByTypeOptions interface + +Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) + +Signature: + +```typescript +export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [hasReference](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md) | SavedObjectsFindOptionsReference[] | optional array of references to search object for. | +| [search](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md) | string | optional query string to filter exported objects. | +| [types](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md) | string[] | array of saved object types. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md new file mode 100644 index 0000000000000..ce8c2c87ddaf7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) > [search](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md) + +## SavedObjectsExportByTypeOptions.search property + +optional query string to filter exported objects. + +Signature: + +```typescript +search?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md new file mode 100644 index 0000000000000..eed71d7f39d23 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) > [types](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md) + +## SavedObjectsExportByTypeOptions.types property + +array of saved object types. + +Signature: + +```typescript +types: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md new file mode 100644 index 0000000000000..23f49a703814f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > ["\#private"](./kibana-plugin-core-server.savedobjectsexporter.__private_.md) + +## SavedObjectsExporter."\#private" property + +Signature: + +```typescript +#private; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md new file mode 100644 index 0000000000000..cc192b03ca7c2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > [(constructor)](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) + +## SavedObjectsExporter.(constructor) + +Constructs a new instance of the `SavedObjectsExporter` class + +Signature: + +```typescript +constructor({ savedObjectsClient, exportSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + exportSizeLimit: number; + }); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { savedObjectsClient, exportSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
exportSizeLimit: number;
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md new file mode 100644 index 0000000000000..a7dc5a71b835d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > [exportByObjects](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) + +## SavedObjectsExporter.exportByObjects() method + +Generates an export stream for given object references. + +See the [options](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) for more detailed information. + +Signature: + +```typescript +exportByObjects(options: SavedObjectsExportByObjectOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | SavedObjectsExportByObjectOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsExportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md new file mode 100644 index 0000000000000..83da41bad7fe0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > [exportByTypes](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) + +## SavedObjectsExporter.exportByTypes() method + +Generates an export stream for given types. + +See the [options](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) for more detailed information. + +Signature: + +```typescript +exportByTypes(options: SavedObjectsExportByTypeOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | SavedObjectsExportByTypeOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsExportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md new file mode 100644 index 0000000000000..d8d9248f34af6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) + +## SavedObjectsExporter class + + +Signature: + +```typescript +export declare class SavedObjectsExporter +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ savedObjectsClient, exportSizeLimit, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| ["\#private"](./kibana-plugin-core-server.savedobjectsexporter.__private_.md) | | | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [exportByObjects(options)](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | Generates an export stream for given object references.See the [options](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) for more detailed information. | +| [exportByTypes(options)](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | | Generates an export stream for given types.See the [options](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) for more detailed information. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md new file mode 100644 index 0000000000000..33bc6113d56e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [(constructor)](./kibana-plugin-core-server.savedobjectsexporterror._constructor_.md) + +## SavedObjectsExportError.(constructor) + +Constructs a new instance of the `SavedObjectsExportError` class + +Signature: + +```typescript +constructor(type: string, message: string, attributes?: Record | undefined); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| message | string | | +| attributes | Record<string, any> | undefined | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.attributes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.attributes.md new file mode 100644 index 0000000000000..9061399eab1f0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [attributes](./kibana-plugin-core-server.savedobjectsexporterror.attributes.md) + +## SavedObjectsExportError.attributes property + +Signature: + +```typescript +readonly attributes?: Record | undefined; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md new file mode 100644 index 0000000000000..c4097724b193d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [exportSizeExceeded](./kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md) + +## SavedObjectsExportError.exportSizeExceeded() method + +Signature: + +```typescript +static exportSizeExceeded(limit: number): SavedObjectsExportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| limit | number | | + +Returns: + +`SavedObjectsExportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md new file mode 100644 index 0000000000000..bfeaa03a94700 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md @@ -0,0 +1,33 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) + +## SavedObjectsExportError class + + +Signature: + +```typescript +export declare class SavedObjectsExportError extends Error +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(type, message, attributes)](./kibana-plugin-core-server.savedobjectsexporterror._constructor_.md) | | Constructs a new instance of the SavedObjectsExportError class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [attributes](./kibana-plugin-core-server.savedobjectsexporterror.attributes.md) | | Record<string, any> | undefined | | +| [type](./kibana-plugin-core-server.savedobjectsexporterror.type.md) | | string | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [exportSizeExceeded(limit)](./kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md) | static | | +| [objectFetchError(objects)](./kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md) | static | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md new file mode 100644 index 0000000000000..afaa4693f3c70 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [objectFetchError](./kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md) + +## SavedObjectsExportError.objectFetchError() method + +Signature: + +```typescript +static objectFetchError(objects: SavedObject[]): SavedObjectsExportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObject[] | | + +Returns: + +`SavedObjectsExportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.type.md new file mode 100644 index 0000000000000..0c1cda48246ad --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [type](./kibana-plugin-core-server.savedobjectsexporterror.type.md) + +## SavedObjectsExportError.type property + +Signature: + +```typescript +readonly type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md deleted file mode 100644 index f1a71eefa8ca7..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [exportSizeLimit](./kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md) - -## SavedObjectsExportOptions.exportSizeLimit property - -the maximum number of objects to export. - -Signature: - -```typescript -exportSizeLimit: number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md deleted file mode 100644 index 9ea9fb2e7fba2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [hasReference](./kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md) - -## SavedObjectsExportOptions.hasReference property - -optional array of references to search object for when exporting by types - -Signature: - -```typescript -hasReference?: SavedObjectsFindOptionsReference[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md deleted file mode 100644 index a45ca30b3cd46..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [includeReferencesDeep](./kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md) - -## SavedObjectsExportOptions.includeReferencesDeep property - -flag to also include all related saved objects in the export stream. - -Signature: - -```typescript -includeReferencesDeep?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.md deleted file mode 100644 index b1b51a123696c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) - -## SavedObjectsExportOptions interface - -Options controlling the export operation. - -Signature: - -```typescript -export interface SavedObjectsExportOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [excludeExportDetails](./kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | -| [exportSizeLimit](./kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md) | number | the maximum number of objects to export. | -| [hasReference](./kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md) | SavedObjectsFindOptionsReference[] | optional array of references to search object for when exporting by types | -| [includeReferencesDeep](./kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | -| [namespace](./kibana-plugin-core-server.savedobjectsexportoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | -| [objects](./kibana-plugin-core-server.savedobjectsexportoptions.objects.md) | Array<{
id: string;
type: string;
}> | optional array of objects to export. | -| [savedObjectsClient](./kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md) | SavedObjectsClientContract | an instance of the SavedObjectsClient. | -| [search](./kibana-plugin-core-server.savedobjectsexportoptions.search.md) | string | optional query string to filter exported objects. | -| [types](./kibana-plugin-core-server.savedobjectsexportoptions.types.md) | string[] | optional array of saved object types. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.objects.md deleted file mode 100644 index b27fe2169e2d3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.objects.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [objects](./kibana-plugin-core-server.savedobjectsexportoptions.objects.md) - -## SavedObjectsExportOptions.objects property - -optional array of objects to export. - -Signature: - -```typescript -objects?: Array<{ - id: string; - type: string; - }>; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md deleted file mode 100644 index 64f3968fa201e..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [savedObjectsClient](./kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md) - -## SavedObjectsExportOptions.savedObjectsClient property - -an instance of the SavedObjectsClient. - -Signature: - -```typescript -savedObjectsClient: SavedObjectsClientContract; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.search.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.search.md deleted file mode 100644 index 0a888d9618012..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.search.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [search](./kibana-plugin-core-server.savedobjectsexportoptions.search.md) - -## SavedObjectsExportOptions.search property - -optional query string to filter exported objects. - -Signature: - -```typescript -search?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.types.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.types.md deleted file mode 100644 index d04ff5fc0aa72..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.types.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [types](./kibana-plugin-core-server.savedobjectsexportoptions.types.md) - -## SavedObjectsExportOptions.types property - -optional array of saved object types. - -Signature: - -```typescript -types?: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md new file mode 100644 index 0000000000000..2d780a957e087 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > ["\#private"](./kibana-plugin-core-server.savedobjectsimporter.__private_.md) + +## SavedObjectsImporter."\#private" property + +Signature: + +```typescript +#private; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md new file mode 100644 index 0000000000000..67df4dbf09ad6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > [(constructor)](./kibana-plugin-core-server.savedobjectsimporter._constructor_.md) + +## SavedObjectsImporter.(constructor) + +Constructs a new instance of the `SavedObjectsImporter` class + +Signature: + +```typescript +constructor({ savedObjectsClient, typeRegistry, importSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + importSizeLimit: number; + }); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { savedObjectsClient, typeRegistry, importSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
importSizeLimit: number;
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md new file mode 100644 index 0000000000000..5b1b2d733fa0e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > [import](./kibana-plugin-core-server.savedobjectsimporter.import.md) + +## SavedObjectsImporter.import() method + +Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. + +Signature: + +```typescript +import({ readStream, createNewCopies, namespace, overwrite, }: SavedObjectsImportOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, createNewCopies, namespace, overwrite, } | SavedObjectsImportOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsImportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md new file mode 100644 index 0000000000000..ad07c23ae7034 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) + +## SavedObjectsImporter class + + +Signature: + +```typescript +export declare class SavedObjectsImporter +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ savedObjectsClient, typeRegistry, importSizeLimit, })](./kibana-plugin-core-server.savedobjectsimporter._constructor_.md) | | Constructs a new instance of the SavedObjectsImporter class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| ["\#private"](./kibana-plugin-core-server.savedobjectsimporter.__private_.md) | | | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [import({ readStream, createNewCopies, namespace, overwrite, })](./kibana-plugin-core-server.savedobjectsimporter.import.md) | | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | +| [resolveImportErrors({ readStream, createNewCopies, namespace, retries, })](./kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md) | | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md new file mode 100644 index 0000000000000..c4ea529d30eff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > [resolveImportErrors](./kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md) + +## SavedObjectsImporter.resolveImportErrors() method + +Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. + +Signature: + +```typescript +resolveImportErrors({ readStream, createNewCopies, namespace, retries, }: SavedObjectsResolveImportErrorsOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, createNewCopies, namespace, retries, } | SavedObjectsResolveImportErrorsOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsImportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.attributes.md similarity index 51% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.id.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.attributes.md index 8ae9f9c377b4e..6d09d4cb88120 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.id.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.attributes.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [id](./kibana-plugin-core-server.savedobjectsimporterror.id.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [attributes](./kibana-plugin-core-server.savedobjectsimporterror.attributes.md) -## SavedObjectsImportError.id property +## SavedObjectsImportError.attributes property Signature: ```typescript -id: string; +readonly attributes?: Record | undefined; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md new file mode 100644 index 0000000000000..9dcc43633d9eb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [importSizeExceeded](./kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md) + +## SavedObjectsImportError.importSizeExceeded() method + +Signature: + +```typescript +static importSizeExceeded(limit: number): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| limit | number | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md index 713e23edef081..b37b6143e7b73 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md @@ -2,24 +2,29 @@ [Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) -## SavedObjectsImportError interface +## SavedObjectsImportError class -Represents a failure to import. Signature: ```typescript -export interface SavedObjectsImportError +export declare class SavedObjectsImportError extends Error ``` ## Properties -| Property | Type | Description | +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [attributes](./kibana-plugin-core-server.savedobjectsimporterror.attributes.md) | | Record<string, any> | undefined | | +| [type](./kibana-plugin-core-server.savedobjectsimporterror.type.md) | | string | | + +## Methods + +| Method | Modifiers | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-server.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | -| [id](./kibana-plugin-core-server.savedobjectsimporterror.id.md) | string | | -| [meta](./kibana-plugin-core-server.savedobjectsimporterror.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-server.savedobjectsimporterror.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | -| [title](./kibana-plugin-core-server.savedobjectsimporterror.title.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectsimporterror.type.md) | string | | +| [importSizeExceeded(limit)](./kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md) | static | | +| [nonUniqueImportObjects(nonUniqueEntries)](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md) | static | | +| [nonUniqueRetryDestinations(nonUniqueRetryDestinations)](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md) | static | | +| [nonUniqueRetryObjects(nonUniqueRetryObjects)](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md) | static | | +| [referencesFetchError(objects)](./kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md new file mode 100644 index 0000000000000..a4a1975af0b4c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [nonUniqueImportObjects](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md) + +## SavedObjectsImportError.nonUniqueImportObjects() method + +Signature: + +```typescript +static nonUniqueImportObjects(nonUniqueEntries: string[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| nonUniqueEntries | string[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md new file mode 100644 index 0000000000000..a60f6c34cb7e2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [nonUniqueRetryDestinations](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md) + +## SavedObjectsImportError.nonUniqueRetryDestinations() method + +Signature: + +```typescript +static nonUniqueRetryDestinations(nonUniqueRetryDestinations: string[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| nonUniqueRetryDestinations | string[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md new file mode 100644 index 0000000000000..187904ccf59a2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [nonUniqueRetryObjects](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md) + +## SavedObjectsImportError.nonUniqueRetryObjects() method + +Signature: + +```typescript +static nonUniqueRetryObjects(nonUniqueRetryObjects: string[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| nonUniqueRetryObjects | string[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md new file mode 100644 index 0000000000000..c9392739838dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [referencesFetchError](./kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md) + +## SavedObjectsImportError.referencesFetchError() method + +Signature: + +```typescript +static referencesFetchError(objects: SavedObject[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObject[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md index e4484bbbe8578..db655f8cfa129 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md @@ -7,5 +7,5 @@ Signature: ```typescript -type: string; +readonly type: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.error.md similarity index 62% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.error.md index 6fc0c86b2fafc..40c9fa1fefa91 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.error.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [error](./kibana-plugin-core-server.savedobjectsimporterror.error.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [error](./kibana-plugin-core-server.savedobjectsimportfailure.error.md) -## SavedObjectsImportError.error property +## SavedObjectsImportFailure.error property Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.id.md new file mode 100644 index 0000000000000..a58183b84e401 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [id](./kibana-plugin-core-server.savedobjectsimportfailure.id.md) + +## SavedObjectsImportFailure.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md new file mode 100644 index 0000000000000..536f48f45e0c5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) + +## SavedObjectsImportFailure interface + +Represents a failure to import. + +Signature: + +```typescript +export interface SavedObjectsImportFailure +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-server.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-core-server.savedobjectsimportfailure.id.md) | string | | +| [meta](./kibana-plugin-core-server.savedobjectsimportfailure.meta.md) | {
title?: string;
icon?: string;
} | | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | +| [title](./kibana-plugin-core-server.savedobjectsimportfailure.title.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectsimportfailure.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.meta.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.meta.md similarity index 51% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.meta.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.meta.md index 8d88bf1e375d4..c345ebe28b945 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.meta.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.meta.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [meta](./kibana-plugin-core-server.savedobjectsimporterror.meta.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [meta](./kibana-plugin-core-server.savedobjectsimportfailure.meta.md) -## SavedObjectsImportError.meta property +## SavedObjectsImportFailure.meta property Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.overwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md similarity index 54% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.overwrite.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md index f706f921cf052..0bd3f1c1d72e8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.overwrite.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [overwrite](./kibana-plugin-core-server.savedobjectsimporterror.overwrite.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [overwrite](./kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md) -## SavedObjectsImportError.overwrite property +## SavedObjectsImportFailure.overwrite property If `overwrite` is specified, an attempt was made to overwrite an existing object. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.title.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.title.md similarity index 53% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.title.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.title.md index 3d787cbe20bb4..12326e6b0e4bb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.title.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.title.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [title](./kibana-plugin-core-server.savedobjectsimporterror.title.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [title](./kibana-plugin-core-server.savedobjectsimportfailure.title.md) -## SavedObjectsImportError.title property +## SavedObjectsImportFailure.title property > Warning: This API is now obsolete. > diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.type.md new file mode 100644 index 0000000000000..ff1529eb8db7a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [type](./kibana-plugin-core-server.savedobjectsimportfailure.type.md) + +## SavedObjectsImportFailure.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md index 6578b01ffa609..ddda72938b13a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md @@ -18,9 +18,6 @@ export interface SavedObjectsImportOptions | --- | --- | --- | | [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | | [namespace](./kibana-plugin-core-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | -| [objectLimit](./kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md) | number | The maximum number of object to import | | [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | If true, will override existing object if present. Note: this has no effect when used with the createNewCopies option. | | [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | -| [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation | -| [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md deleted file mode 100644 index 21b86d825502c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [objectLimit](./kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md) - -## SavedObjectsImportOptions.objectLimit property - -The maximum number of object to import - -Signature: - -```typescript -objectLimit: number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md deleted file mode 100644 index 2ae7c350d188b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) - -## SavedObjectsImportOptions.savedObjectsClient property - -[client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation - -Signature: - -```typescript -savedObjectsClient: SavedObjectsClientContract; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md deleted file mode 100644 index 89c49471d24ef..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) - -## SavedObjectsImportOptions.typeRegistry property - -The registry of all known saved object types - -Signature: - -```typescript -typeRegistry: ISavedObjectTypeRegistry; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md index ee2e86c9e4b24..dc6f782fc937f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md @@ -7,5 +7,5 @@ Signature: ```typescript -errors?: SavedObjectsImportError[]; +errors?: SavedObjectsImportFailure[]; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md index 52d39d981d0c2..94d24e946b5bd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportResponse | Property | Type | Description | | --- | --- | --- | -| [errors](./kibana-plugin-core-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | +| [errors](./kibana-plugin-core-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure[] | | | [success](./kibana-plugin-core-server.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-server.savedobjectsimportresponse.successcount.md) | number | | | [successResults](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md index f97bf284375d1..dcd2305c831f4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md @@ -18,9 +18,6 @@ export interface SavedObjectsResolveImportErrorsOptions | --- | --- | --- | | [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | | [namespace](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | if specified, will import in given namespace | -| [objectLimit](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | The maximum number of object to import | | [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | | [retries](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | -| [savedObjectsClient](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | client to use to perform the import operation | -| [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md deleted file mode 100644 index 156fe96029275..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [objectLimit](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) - -## SavedObjectsResolveImportErrorsOptions.objectLimit property - -The maximum number of object to import - -Signature: - -```typescript -objectLimit: number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md deleted file mode 100644 index b338c132addf2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [savedObjectsClient](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) - -## SavedObjectsResolveImportErrorsOptions.savedObjectsClient property - -client to use to perform the import operation - -Signature: - -```typescript -savedObjectsClient: SavedObjectsClientContract; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md deleted file mode 100644 index f06d3eb08c0ac..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) - -## SavedObjectsResolveImportErrorsOptions.typeRegistry property - -The registry of all known saved object types - -Signature: - -```typescript -typeRegistry: ISavedObjectTypeRegistry; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md deleted file mode 100644 index 792a0ac3d9420..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) > [getImportExportObjectLimit](./kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md) - -## SavedObjectsServiceSetup.getImportExportObjectLimit property - -Returns the maximum number of objects allowed for import or export operations. - -Signature: - -```typescript -getImportExportObjectLimit: () => number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md index 650459bfdb435..56ebb48707f59 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md @@ -52,7 +52,6 @@ export class Plugin() { | Property | Type | Description | | --- | --- | --- | | [addClientWrapper](./kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) with the given priority. | -| [getImportExportObjectLimit](./kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md) | () => number | Returns the maximum number of objects allowed for import or export operations. | | [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | (type: SavedObjectsType) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | | [setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createexporter.md new file mode 100644 index 0000000000000..273d80983f15d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createexporter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) > [createExporter](./kibana-plugin-core-server.savedobjectsservicestart.createexporter.md) + +## SavedObjectsServiceStart.createExporter property + +Creates an [exporter](./kibana-plugin-core-server.isavedobjectsexporter.md) bound to given client. + +Signature: + +```typescript +createExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createimporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createimporter.md new file mode 100644 index 0000000000000..f2617c5c6c12a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createimporter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) > [createImporter](./kibana-plugin-core-server.savedobjectsservicestart.createimporter.md) + +## SavedObjectsServiceStart.createImporter property + +Creates an [importer](./kibana-plugin-core-server.isavedobjectsimporter.md) bound to given client. + +Signature: + +```typescript +createImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md index 17655bb4878a7..075a363fe1aa2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md @@ -16,6 +16,8 @@ export interface SavedObjectsServiceStart | Property | Type | Description | | --- | --- | --- | +| [createExporter](./kibana-plugin-core-server.savedobjectsservicestart.createexporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsExporter | Creates an [exporter](./kibana-plugin-core-server.isavedobjectsexporter.md) bound to given client. | +| [createImporter](./kibana-plugin-core-server.savedobjectsservicestart.createimporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsImporter | Creates an [importer](./kibana-plugin-core-server.isavedobjectsimporter.md) bound to given client. | | [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) | (includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | | [createScopedRepository](./kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | | [createSerializer](./kibana-plugin-core-server.savedobjectsservicestart.createserializer.md) | () => SavedObjectsSerializer | Creates a [serializer](./kibana-plugin-core-server.savedobjectsserializer.md) that is aware of all registered types. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index a370c67f460f4..6768712f38529 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -18,6 +18,6 @@ export interface ISearchSetup | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | | [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | -| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | ISessionsClient | Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md index d9af202cf1018..4c3c10dec6ab9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md @@ -4,7 +4,7 @@ ## ISearchSetup.sessionsClient property -Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) +Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index a27e155dda111..34a7614ff2ae3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -20,6 +20,6 @@ export interface ISearchStart | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | | [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | -| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | ISessionsClient | Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md index 9c3210d2ec417..2248a9b2f8229 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md @@ -4,7 +4,7 @@ ## ISearchStart.sessionsClient property -Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) +Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 2040043d4351b..6a3e7662e59bc 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -34,7 +34,7 @@ | [KBN\_FIELD\_TYPES](./kibana-plugin-plugins-data-public.kbn_field_types.md) | \* | | [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | | | [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | | -| [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) | Possible state that current session can be in | +| [SearchSessionState](./kibana-plugin-plugins-data-public.searchsessionstate.md) | Possible state that current session can be in | | [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | | | [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | | @@ -90,7 +90,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | -| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | +| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md index 2a5e1d2a3135f..75351434a7bb9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md @@ -4,7 +4,7 @@ ## SearchSessionInfoProvider.getName property -User-facing name of the session. e.g. will be displayed in background sessions management list +User-facing name of the session. e.g. will be displayed in saved Search Sessions management list Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md index bcc4a5508eb59..77125bc8deead 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md @@ -4,7 +4,7 @@ ## SearchSessionInfoProvider interface -Provide info about current search session to be stored in backgroundSearch saved object +Provide info about current search session to be stored in the Search Session saved object Signature: @@ -16,6 +16,6 @@ export interface SearchSessionInfoProvider() => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | +| [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | () => Promise<string> | User-facing name of the session. e.g. will be displayed in saved Search Sessions management list | | [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionstate.md similarity index 71% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionstate.md index 9a60a5b2a9f9b..c650ec6b26166 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionstate.md @@ -1,25 +1,25 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionState](./kibana-plugin-plugins-data-public.searchsessionstate.md) -## SessionState enum +## SearchSessionState enum Possible state that current session can be in Signature: ```typescript -export declare enum SessionState +export declare enum SearchSessionState ``` ## Enumeration Members | Member | Value | Description | | --- | --- | --- | -| BackgroundCompleted | "backgroundCompleted" | Page load completed with background session created. | -| BackgroundLoading | "backgroundLoading" | Search request was sent to the background. The page is loading in background. | +| BackgroundCompleted | "backgroundCompleted" | Page load completed with search session created. | +| BackgroundLoading | "backgroundLoading" | Search session was sent to the background. The page is loading in background. | | Canceled | "canceled" | Current session requests where explicitly canceled by user Displaying none or partial results | -| Completed | "completed" | No action was taken and the page completed loading without background session creation. | +| Completed | "completed" | No action was taken and the page completed loading without search session creation. | | Loading | "loading" | Pending search request has not been sent to the background yet | | None | "none" | Session is not active, e.g. didn't start | | Restored | "restored" | Revisiting the page after background completion | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.extend.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.extend.md new file mode 100644 index 0000000000000..65e3c2868f29f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.extend.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) > [extend](./kibana-plugin-plugins-data-server.isearchstrategy.extend.md) + +## ISearchStrategy.extend property + +Signature: + +```typescript +extend?: (id: string, keepAlive: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md index c9f4c886735a7..c46a580d5ceb8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -17,5 +17,6 @@ export interface ISearchStrategy(id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise<void> | | +| [extend](./kibana-plugin-plugins-data-server.isearchstrategy.extend.md) | (id: string, keepAlive: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise<void> | | | [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable<SearchStrategyResponse> | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.contextmenutrigger.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.contextmenutrigger.md index 0a88e1e0a2ea8..eec1e9ac7e3fb 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.contextmenutrigger.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.contextmenutrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> +contextMenuTrigger: Trigger ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.supportedtriggers.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.supportedtriggers.md index 16676bc732b1c..8a5efe60ba411 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.supportedtriggers.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.supportedtriggers.md @@ -7,9 +7,9 @@ Signature: ```typescript -supportedTriggers(): Array; +supportedTriggers(): string[]; ``` Returns: -`Array` +`string[]` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md index 0f14215ff1309..07ede291e33d2 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md @@ -17,5 +17,6 @@ export declare type EmbeddableInput = { disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; + syncColors?: boolean; }; ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.supportedtriggers.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.supportedtriggers.md index 5480f3b246648..bb560c11bf440 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.supportedtriggers.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.supportedtriggers.md @@ -9,9 +9,9 @@ List of triggers that this embeddable will execute. Signature: ```typescript -supportedTriggers(): Array; +supportedTriggers(): string[]; ``` Returns: -`Array` +`string[]` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md index ce97f79b4beb9..add4646375359 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md @@ -14,7 +14,7 @@ export declare function openAddPanelFlyout(options: { overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; -}): Promise; +}): OverlayRef; ``` ## Parameters @@ -25,5 +25,5 @@ export declare function openAddPanelFlyout(options: { Returns: -`Promise` +`OverlayRef` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelbadgetrigger.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelbadgetrigger.md index f6113c93a1c66..feacd0152d384 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelbadgetrigger.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelbadgetrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> +panelBadgeTrigger: Trigger ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelnotificationtrigger.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelnotificationtrigger.md index df606c11f64ce..c831df19d2959 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelnotificationtrigger.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.panelnotificationtrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> +panelNotificationTrigger: Trigger ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md index 1565202e84674..9dfad91c33679 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `ExpressionRenderHandler` class Signature: ```typescript -constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams); +constructor(element: HTMLElement, { onRenderError, renderMode, syncColors, hasCompatibleActions, }?: ExpressionRenderHandlerParams); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActi | Parameter | Type | Description | | --- | --- | --- | | element | HTMLElement | | -| { onRenderError, renderMode, hasCompatibleActions, } | ExpressionRenderHandlerParams | | +| { onRenderError, renderMode, syncColors, hasCompatibleActions, } | ExpressionRenderHandlerParams | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md index d65c06bdaed83..1a7050f3ffd4e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md @@ -14,7 +14,7 @@ export declare class ExpressionRenderHandler | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(element, { onRenderError, renderMode, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the ExpressionRenderHandler class | +| [(constructor)(element, { onRenderError, renderMode, syncColors, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the ExpressionRenderHandler class | ## Properties diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index 22a73fff039e6..4ef1225ae0d7e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -25,6 +25,7 @@ export interface IExpressionLoaderParams | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | | [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | +| [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) | boolean | | | [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | unknown | | | [variables](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.variables.md) | Record<string, any> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md new file mode 100644 index 0000000000000..619f54ad88ef2 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) + +## IExpressionLoaderParams.syncColors property + +Signature: + +```typescript +syncColors?: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md new file mode 100644 index 0000000000000..6cdc796bf464b --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) > [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md) + +## IInterpreterRenderHandlers.isSyncColorsEnabled property + +Signature: + +```typescript +isSyncColorsEnabled: () => boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md index c22c8bc6b6245..0b39a9b4b3ea2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md @@ -18,6 +18,7 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode | | | [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean> | | +| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md) | () => boolean | | | [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | () => void | | | [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | unknown | This uiState interface is actually PersistedState from the visualizations plugin, but expressions cannot know about vis or it creates a mess of circular dependencies. Downstream consumers of the uiState handler will need to cast for now. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md new file mode 100644 index 0000000000000..71a7e020e65a5 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) > [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md) + +## IInterpreterRenderHandlers.isSyncColorsEnabled property + +Signature: + +```typescript +isSyncColorsEnabled: () => boolean; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md index 547608f40e6aa..831c9023c7e48 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md @@ -18,6 +18,7 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode | | | [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean> | | +| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md) | () => boolean | | | [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | () => void | | | [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | unknown | This uiState interface is actually PersistedState from the visualizations plugin, but expressions cannot know about vis or it creates a mess of circular dependencies. Downstream consumers of the uiState handler will need to cast for now. | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.md index 19af63a679de8..d8e527debcc4e 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface Action extends Partial>> +export interface Action extends Partial>> ``` ## Properties @@ -17,7 +17,7 @@ export interface Action extend | [id](./kibana-plugin-plugins-ui_actions-public.action.id.md) | string | A unique identifier for this action instance. | | [MenuItem](./kibana-plugin-plugins-ui_actions-public.action.menuitem.md) | UiComponent<{
context: ActionExecutionContext<Context>;
}> | UiComponent to render when displaying this action as a context menu item. If not provided, getDisplayName will be used instead. | | [order](./kibana-plugin-plugins-ui_actions-public.action.order.md) | number | Determined the order when there is more than one action matched to a trigger. Higher numbers are displayed first. | -| [type](./kibana-plugin-plugins-ui_actions-public.action.type.md) | T | The action type is what determines the context shape. | +| [type](./kibana-plugin-plugins-ui_actions-public.action.type.md) | string | The action type is what determines the context shape. | ## Methods diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.type.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.type.md index c423df9d1324c..6905f3deb441d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.type.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action.type.md @@ -9,5 +9,5 @@ The action type is what determines the context shape. Signature: ```typescript -readonly type: T; +readonly type: string; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actionbytype.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actionbytype.md deleted file mode 100644 index 3ceb96adadb1a..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actionbytype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionByType](./kibana-plugin-plugins-ui_actions-public.actionbytype.md) - -## ActionByType type - -Signature: - -```typescript -export declare type ActionByType = Action; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.__.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.__.md deleted file mode 100644 index eb7b1e5954ed2..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.__.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) > [""](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.__.md) - -## ActionContextMapping."" property - -Signature: - -```typescript -[DEFAULT_ACTION]: BaseContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_field.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_field.md deleted file mode 100644 index eb0547bbf8261..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_field.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) > [ACTION\_VISUALIZE\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_field.md) - -## ActionContextMapping.ACTION\_VISUALIZE\_FIELD property - -Signature: - -```typescript -[ACTION_VISUALIZE_FIELD]: VisualizeFieldContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_geo_field.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_geo_field.md deleted file mode 100644 index b44ed75106423..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_geo_field.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) > [ACTION\_VISUALIZE\_GEO\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_geo_field.md) - -## ActionContextMapping.ACTION\_VISUALIZE\_GEO\_FIELD property - -Signature: - -```typescript -[ACTION_VISUALIZE_GEO_FIELD]: VisualizeFieldContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md deleted file mode 100644 index 96370a07806d3..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) > [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md) - -## ActionContextMapping.ACTION\_VISUALIZE\_LENS\_FIELD property - -Signature: - -```typescript -[ACTION_VISUALIZE_LENS_FIELD]: VisualizeFieldContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md deleted file mode 100644 index f83632dea0aa9..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) - -## ActionContextMapping interface - -Signature: - -```typescript -export interface ActionContextMapping -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [""](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.__.md) | BaseContext | | -| [ACTION\_VISUALIZE\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_field.md) | VisualizeFieldContext | | -| [ACTION\_VISUALIZE\_GEO\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_geo_field.md) | VisualizeFieldContext | | -| [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md) | VisualizeFieldContext | | - diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actiondefinitionbytype.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actiondefinitionbytype.md deleted file mode 100644 index ba4dc39088fe4..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actiondefinitionbytype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionDefinitionByType](./kibana-plugin-plugins-ui_actions-public.actiondefinitionbytype.md) - -## ActionDefinitionByType type - -Signature: - -```typescript -export declare type ActionDefinitionByType = ActionDefinition; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actionexecutioncontext.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actionexecutioncontext.md index 3271d86779959..d6f754a1ba458 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actionexecutioncontext.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actionexecutioncontext.md @@ -9,5 +9,5 @@ Action methods are executed with Context from trigger + [ActionExecutionMeta](./ Signature: ```typescript -export declare type ActionExecutionContext = Context & ActionExecutionMeta; +export declare type ActionExecutionContext = Context & ActionExecutionMeta; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actiontype.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actiontype.md deleted file mode 100644 index 4916585531004..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actiontype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionType](./kibana-plugin-plugins-ui_actions-public.actiontype.md) - -## ActionType type - -Signature: - -```typescript -export declare type ActionType = keyof ActionContextMapping; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.createaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.createaction.md index 04ab36c2e3f58..8bb9094a1d8bf 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.createaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.createaction.md @@ -7,16 +7,16 @@ Signature: ```typescript -export declare function createAction(action: ActionDefinitionByType): ActionByType; +export declare function createAction(action: ActionDefinition): Action; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| action | ActionDefinitionByType<T> | | +| action | ActionDefinition<Context> | | Returns: -`ActionByType` +`Action` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md index 76e347bddd168..9f009d1617cc8 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md @@ -24,11 +24,9 @@ | Interface | Description | | --- | --- | | [Action](./kibana-plugin-plugins-ui_actions-public.action.md) | | -| [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) | | | [ActionExecutionMeta](./kibana-plugin-plugins-ui_actions-public.actionexecutionmeta.md) | During action execution we can provide additional information, for example, trigger, that caused the action execution | | [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) | | | [Trigger](./kibana-plugin-plugins-ui_actions-public.trigger.md) | This is a convenience interface used to register a \*trigger\*.Trigger specifies a named anchor to which Action can be attached. When Trigger is being \*called\* it creates a Context object and passes it to the execute method of an Action.More than one action can be attached to a single trigger, in which case when trigger is \*called\* it first displays a context menu for user to pick a single action to execute. | -| [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) | | | [UiActionsActionDefinition](./kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md) | A convenience interface used to register an action. | | [UiActionsPresentable](./kibana-plugin-plugins-ui_actions-public.uiactionspresentable.md) | Represents something that can be displayed to user in UI. | | [UiActionsServiceParams](./kibana-plugin-plugins-ui_actions-public.uiactionsserviceparams.md) | | @@ -52,12 +50,7 @@ | Type Alias | Description | | --- | --- | -| [ActionByType](./kibana-plugin-plugins-ui_actions-public.actionbytype.md) | | -| [ActionDefinitionByType](./kibana-plugin-plugins-ui_actions-public.actiondefinitionbytype.md) | | | [ActionExecutionContext](./kibana-plugin-plugins-ui_actions-public.actionexecutioncontext.md) | Action methods are executed with Context from trigger + [ActionExecutionMeta](./kibana-plugin-plugins-ui_actions-public.actionexecutionmeta.md) | -| [ActionType](./kibana-plugin-plugins-ui_actions-public.actiontype.md) | | -| [TriggerContext](./kibana-plugin-plugins-ui_actions-public.triggercontext.md) | | -| [TriggerId](./kibana-plugin-plugins-ui_actions-public.triggerid.md) | | | [UiActionsPresentableGrouping](./kibana-plugin-plugins-ui_actions-public.uiactionspresentablegrouping.md) | | | [UiActionsSetup](./kibana-plugin-plugins-ui_actions-public.uiactionssetup.md) | | | [UiActionsStart](./kibana-plugin-plugins-ui_actions-public.uiactionsstart.md) | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md index aa1097d8c0864..f05138296e6e8 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'> +rowClickTrigger: Trigger ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.id.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.id.md index 5603c852ad39d..5bf868720cdec 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.id.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.id.md @@ -9,5 +9,5 @@ Unique name of the trigger as identified in `ui_actions` plugin trigger registry Signature: ```typescript -id: ID; +id: string; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.md index ed76cfea97684..d829d7b87c177 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.trigger.md @@ -13,7 +13,7 @@ More than one action can be attached to a single trigger, in which case when tri Signature: ```typescript -export interface Trigger +export interface Trigger ``` ## Properties @@ -21,6 +21,6 @@ export interface Trigger | Property | Type | Description | | --- | --- | --- | | [description](./kibana-plugin-plugins-ui_actions-public.trigger.description.md) | string | A longer user friendly description of the trigger. | -| [id](./kibana-plugin-plugins-ui_actions-public.trigger.id.md) | ID | Unique name of the trigger as identified in ui_actions plugin trigger registry. | +| [id](./kibana-plugin-plugins-ui_actions-public.trigger.id.md) | string | Unique name of the trigger as identified in ui_actions plugin trigger registry. | | [title](./kibana-plugin-plugins-ui_actions-public.trigger.title.md) | string | User friendly name of the trigger. | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontext.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontext.md deleted file mode 100644 index 4ce95d27ecffa..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontext.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContext](./kibana-plugin-plugins-ui_actions-public.triggercontext.md) - -## TriggerContext type - -Signature: - -```typescript -export declare type TriggerContext = T extends TriggerId ? TriggerContextMapping[T] : never; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md deleted file mode 100644 index 17ad926f8ee82..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) > [""](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md) - -## TriggerContextMapping."" property - -Signature: - -```typescript -[DEFAULT_TRIGGER]: TriggerContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md deleted file mode 100644 index da7a7a8bfe645..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) - -## TriggerContextMapping interface - -Signature: - -```typescript -export interface TriggerContextMapping -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [""](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md) | TriggerContext | | -| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md) | RowClickContext | | -| [VISUALIZE\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md) | VisualizeFieldContext | | -| [VISUALIZE\_GEO\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_geo_field_trigger.md) | VisualizeFieldContext | | - diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md deleted file mode 100644 index cf253df337378..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) > [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md) - -## TriggerContextMapping.ROW\_CLICK\_TRIGGER property - -Signature: - -```typescript -[ROW_CLICK_TRIGGER]: RowClickContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md deleted file mode 100644 index feaaffac8a234..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) > [VISUALIZE\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md) - -## TriggerContextMapping.VISUALIZE\_FIELD\_TRIGGER property - -Signature: - -```typescript -[VISUALIZE_FIELD_TRIGGER]: VisualizeFieldContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_geo_field_trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_geo_field_trigger.md deleted file mode 100644 index 023490a2ae027..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_geo_field_trigger.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) > [VISUALIZE\_GEO\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_geo_field_trigger.md) - -## TriggerContextMapping.VISUALIZE\_GEO\_FIELD\_TRIGGER property - -Signature: - -```typescript -[VISUALIZE_GEO_FIELD_TRIGGER]: VisualizeFieldContext; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggerid.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggerid.md deleted file mode 100644 index 6e5a234e286f9..0000000000000 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggerid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerId](./kibana-plugin-plugins-ui_actions-public.triggerid.md) - -## TriggerId type - -Signature: - -```typescript -export declare type TriggerId = keyof TriggerContextMapping; -``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md index 7c873715795e9..a4de28ff4d1af 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md @@ -9,7 +9,7 @@ A convenience interface used to register an action. Signature: ```typescript -export interface ActionDefinition extends Partial>> +export interface ActionDefinition extends Partial>> ``` ## Properties @@ -17,7 +17,7 @@ export interface ActionDefinition extends Part | Property | Type | Description | | --- | --- | --- | | [id](./kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.id.md) | string | ID of the action that uniquely identifies this action in the actions registry. | -| [type](./kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.type.md) | ActionType | ID of the factory for this action. Used to construct dynamic actions. | +| [type](./kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.type.md) | string | ID of the factory for this action. Used to construct dynamic actions. | ## Methods diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.type.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.type.md index 125f834e9036e..c2cc8b41568ce 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.type.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.type.md @@ -9,5 +9,5 @@ ID of the factory for this action. Used to construct dynamic actions. Signature: ```typescript -readonly type?: ActionType; +readonly type?: string; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentable.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentable.md index 03fa7fb6e447e..659ea999b9f8e 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentable.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentable.md @@ -9,7 +9,7 @@ Represents something that can be displayed to user in UI. Signature: ```typescript -export interface Presentable +export interface Presentable ``` ## Properties diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentablegrouping.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentablegrouping.md index a61ff65e39c69..2fb6c3e187d3d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentablegrouping.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionspresentablegrouping.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type PresentableGrouping = Array>; +export declare type PresentableGrouping = Array>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index f29d487d774e0..1831c2c78b365 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -6,10 +6,8 @@ `addTriggerAction` is similar to `attachAction` as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet. -`addTriggerAction` also infers better typing of the `action` argument. - Signature: ```typescript -readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; +readonly addTriggerAction: (triggerId: string, action: ActionDefinition) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md index 1ebb30c49c0b3..fd17c76b0ee9f 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly attachAction: (triggerId: T, actionId: string) => void; +readonly attachAction: (triggerId: string, actionId: string) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md index a6ff2489c6f0e..bf9c589e59f60 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly detachAction: (triggerId: TriggerId, actionId: string) => void; +readonly detachAction: (triggerId: string, actionId: string) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md index b20f08520c43d..fb1a1ef14d315 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md @@ -12,5 +12,5 @@ Signature: ```typescript -readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise; +readonly executeTriggerActions: (triggerId: string, context: object) => Promise; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md index 300c46a47c47f..32a4fcf8e6f89 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel">; +readonly getAction: >(id: string) => Action>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md index 95b737a8d6cae..b8f59e943f38e 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTrigger: (triggerId: T) => TriggerContract; +readonly getTrigger: (triggerId: string) => TriggerContract; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md index 27c1b1eb48f16..c7c0eac755aec 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerActions: (triggerId: T) => Action[]; +readonly getTriggerActions: (triggerId: string) => Action[]; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md index edb7d2d3a1551..9e3e38a6ac43d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; +readonly getTriggerCompatibleActions: (triggerId: string, context: object) => Promise; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index 4fe8431770dea..20c237fabd074 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,19 +21,19 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "ROW_CLICK_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "FILTER_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | -| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <T extends "" | "ROW_CLICK_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "FILTER_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER">(triggerId: T, actionId: string) => void | | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | (triggerId: string, action: ActionDefinition) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet. | +| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | (triggerId: string, actionId: string) => void | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void | Removes all registered triggers and actions. | -| [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: TriggerId, actionId: string) => void | | -| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <T extends "" | "ROW_CLICK_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "FILTER_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void> | | +| [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: string, actionId: string) => void | | +| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | (triggerId: string, context: object) => Promise<void> | | | [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | UiActionsExecutionService | | | [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | () => UiActionsService | "Fork" a separate instance of UiActionsService that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of UiActionsService are only available within this instance. | -| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel"> | | -| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <T extends "" | "ROW_CLICK_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "FILTER_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER">(triggerId: T) => TriggerContract<T> | | -| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "ROW_CLICK_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "FILTER_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel">[] | | -| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "ROW_CLICK_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "FILTER_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel">[]> | | +| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<object>>(id: string) => Action<ActionContext<T>> | | +| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | (triggerId: string) => TriggerContract | | +| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | (triggerId: string) => Action[] | | +| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | (triggerId: string, context: object) => Promise<Action[]> | | | [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | (actionId: string) => boolean | | -| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel"> | | +| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<object>>(definition: A) => Action<ActionContext<A>> | | | [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | (trigger: Trigger) => void | | | [triggers](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggers.md) | | TriggerRegistry | | | [triggerToActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggertoactions.md) | | TriggerToActionsRegistry | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md index dee5f75f7c074..75289e8f32351 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel">; +readonly registerAction: >(definition: A) => Action>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizefieldtrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizefieldtrigger.md index 15510bd3eb4a3..eb62d36df84d8 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizefieldtrigger.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizefieldtrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -visualizeFieldTrigger: Trigger<'VISUALIZE_FIELD_TRIGGER'> +visualizeFieldTrigger: Trigger ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizegeofieldtrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizegeofieldtrigger.md index faec6a69b71f9..c547c33aaccbf 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizegeofieldtrigger.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.visualizegeofieldtrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -visualizeGeoFieldTrigger: Trigger<'VISUALIZE_GEO_FIELD_TRIGGER'> +visualizeGeoFieldTrigger: Trigger ``` diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index c1d287fca1f44..feb16190cb34b 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -111,11 +111,11 @@ tags:(success and info and security) [discrete] === Range queries -KQL supports `>`, `>=`, `<`, and `<=`. For example: +KQL supports `>`, `>=`, `<`, and `<=` on numeric and date types. For example: [source,yaml] ------------------- -account_number >= 100 and items_sold <= 200 +account_number >= 100 and items_sold <= 200 and @timestamp >= now-5m ------------------- [discrete] diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 75c6fddb484ac..45f0df5bd773f 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -74,7 +74,7 @@ status codes, you could enter `status:[400 TO 499]`. codes and have an extension of `php` or `html`, you could enter `status:[400 TO 499] AND (extension:php OR extension:html)`. -IMPORTANT: When you use the Lucene Query Syntax in the *KQL* search bar, {kib} is unable to search on nested objects and perform aggregations across fields that contain nested objects. +IMPORTANT: When you use the Lucene Query Syntax in the *KQL* search bar, {kib} is unable to search on nested objects and perform aggregations across fields that contain nested objects. Using `include_in_parent` or `copy_to` as a workaround can cause {kib} to fail. For more detailed information about the Lucene query syntax, see the @@ -107,7 +107,8 @@ To save the current search: . Click *Save* in the Kibana toolbar. . Enter a name for the search and click *Save*. -To import, export, and delete saved searches, open the main menu, then click *Stack Management > Saved Ojbects*. +To import, export, and delete saved searches, open the main menu, +then click *Stack Management > Saved Objects*. ==== Open a saved search To load a saved search into Discover: diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 99fadb240335a..7e7c8953fd527 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -262,6 +262,10 @@ Hides the "Time" column in *Discover* and in all saved searches on dashboards. Highlights results in *Discover* and saved searches on dashboards. Highlighting slows requests when working on big documents. +[[doctable-legacy]]`doc_table:legacy`:: +Control the way the Discover's table looks and works. Set this property to `true` to revert to the legacy implementation. + + [float] [[kibana-ml-settings]] ==== Machine learning diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 2e081c09e0e70..9e26abca115fc 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -60,8 +60,8 @@ You have two options for exporting saved objects. * Select the checkboxes of objects that you want to export, and then click *Export*. * Click *Export x objects*, and export objects by type. -This action creates an NDJSON with all your saved objects. By default, -the NDJSON includes related objects. Exported dashboards include their associated index patterns. +This action creates an NDJSON with all your saved objects. By default, the NDJSON includes child objects that are related to the saved +objects. Exported dashboards include their associated index patterns. [float] [role="xpack"] @@ -73,8 +73,8 @@ and select *Copy to space*. From here, you can select the spaces in which to cop You can also select whether to automatically overwrite any conflicts in the target spaces, or resolve them manually. -WARNING: The copy operation automatically includes related objects. If you don't want this behavior, -use the <> instead. +WARNING: The copy operation automatically includes child objects that are related to the saved objects. If you don't want this behavior, use +the <> instead. [float] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 931a783654a91..2f605f3af01b8 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -198,3 +198,8 @@ This page has moved. Refer to <>. === Variables This page has moved. Refer to <>. + +[role="exclude",id="visualize"] +== Visualize + +This page has been removed. Refer to <>. \ No newline at end of file diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 2d91eb07c5236..8c16c76c62569 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -195,11 +195,11 @@ a| `xpack.reporting.capture.browser` Defaults to `false`. a| `xpack.reporting.capture.browser` -.chromium.proxy.server` +`.chromium.proxy.server` | The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. a| `xpack.reporting.capture.browser` -.chromium.proxy.bypass` +`.chromium.proxy.bypass` | An array of hosts that should not go through the proxy server and should use a direct connection instead. Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601". diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index 9d9cc92401896..4a211976be8cf 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -19,5 +19,6 @@ include::logs-ui-settings.asciidoc[] include::ml-settings.asciidoc[] include::reporting-settings.asciidoc[] include::spaces-settings.asciidoc[] +include::task-manager-settings.asciidoc[] include::i18n-settings.asciidoc[] include::fleet-settings.asciidoc[] diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc new file mode 100644 index 0000000000000..507e54349276b --- /dev/null +++ b/docs/settings/task-manager-settings.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[task-manager-settings-kb]] +=== Task Manager settings in {kib} +++++ +Task Manager settings +++++ + +Task Manager runs background tasks by polling for work on an interval. You can configure its behavior to tune for performance and throughput. + +[float] +[[task-manager-settings]] +==== Task Manager settings + +[cols="2*<"] +|=== +| `xpack.task_manager.max_attempts` + | The maximum number of times a task will be attempted before being abandoned as failed. Defaults to 3. + +| `xpack.task_manager.poll_interval` + | How often, in milliseconds, the task manager will look for more work. Defaults to 3000 and cannot be lower than 100. + +| `xpack.task_manager.request_capacity` + | How many requests can Task Manager buffer before it rejects new requests. Defaults to 1000. + +| `xpack.task_manager.index` + | The name of the index used to store task information. Defaults to `.kibana_task_manager`. + + | `xpack.task_manager.max_workers` + | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. + + +|=== diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 0dee112d15e86..5d79a81e0aa91 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -129,7 +129,7 @@ images: [horizontal] `server.name`:: `kibana` -`server.host`:: `"0"` +`server.host`:: `"0.0.0.0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 4eeecad079348..06370c64aedf8 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -57,7 +57,7 @@ Alert schedules are defined as an interval between subsequent checks, and can ra [IMPORTANT] ============================================== -The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. ============================================== [float] @@ -123,14 +123,15 @@ image::images/alert-concepts-connectors.svg[Connectors provide a central place t [float] === Summary -An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. +An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. The following example ties these concepts together: image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] -* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. -* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. -* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. -* *Connector*: centralized configurations for services and third party integration that are referenced by actions. +. Anytime an *alert*'s conditions are met, an *alert instance* is created. This example checks for servers with average CPU > 0.9. Three servers meet the condition, so three instances are created. +. Instances create *actions* as long as they are not muted or throttled. When actions are created, the template that was setup in the alert is filled with actual values. In this example three actions are created, and the template string {{server}} is replaced with the server name for each instance. +. {kib} invokes the actions, sending them to a 3rd party *integration* like an email service. +. If the 3rd party integration has connection parameters or credentials, {kib} will fetch these from the *connector* referenced in the action. + [float] [[alerting-concepts-differences]] diff --git a/docs/user/alerting/alerting-scale-performance.asciidoc b/docs/user/alerting/alerting-production-considerations.asciidoc similarity index 65% rename from docs/user/alerting/alerting-scale-performance.asciidoc rename to docs/user/alerting/alerting-production-considerations.asciidoc index 644a7143f8278..3a68e81879e24 100644 --- a/docs/user/alerting/alerting-scale-performance.asciidoc +++ b/docs/user/alerting/alerting-production-considerations.asciidoc @@ -1,10 +1,10 @@ [role="xpack"] -[[alerting-scale-performance]] -== Scale and performance +[[alerting-production-considerations]] +== Production considerations -{kib} alerting run both alert checks and actions as persistent background tasks. This has two major benefits: +{kib} alerting run both alert checks and actions as persistent background tasks managed by the Kibana Task Manager. This has two major benefits: -* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. +* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. Task definitions for alerts and actions are stored in the index specified by `xpack.task_manager.index` (defaults to `.kibana_task_manager`). It is important to have at least 1 replica of this index for production deployments, since if you lose this index all scheduled alerts and actions are also lost. * *Scaling*: multiple {kib} instances can read from and update the same task queue in {es}, allowing the alerting and action load to be distributed across instances. In cases where a {kib} instance no longer has capacity to run alert checks or actions, capacity can be increased by adding additional {kib} instances. [float] @@ -12,17 +12,19 @@ {kib} background tasks are managed by: -* Polling an {es} task index for overdue tasks at 3 second intervals. +* Polling an {es} task index for overdue tasks at 3 second intervals. This interval can be changed using the `xpack.task_manager.poll_interval` setting. * Tasks are then claiming them by updating them in the {es} index, using optimistic concurrency control to prevent conflicts. Each {kib} instance can run a maximum of 10 concurrent tasks, so a maximum of 10 tasks are claimed each interval. * Tasks are run on the {kib} server. * In the case of alerts which are recurring background checks, upon completion the task is scheduled again according to the <>. [IMPORTANT] ============================================== -Because tasks are polled at 3 second intervals and only 10 tasks can run concurrently per {kib} instance, it is possible for alert and action tasks to be run late. This can happen if: +Because by default tasks are polled at 3 second intervals and only 10 tasks can run concurrently per {kib} instance, it is possible for alert and action tasks to be run late. This can happen if: * Alerts use a small *check interval*. The lowest interval possible is 3 seconds, though intervals of 30 seconds or higher are recommended. * Many alerts or actions must be *run at once*. In this case pending tasks will queue in {es}, and be pulled 10 at a time from the queue at 3 second intervals. * *Long running tasks* occupy slots for an extended time, leaving fewer slots for other tasks. +For details on the settings that can influence the performance and throughput of Task Manager, see {task-manager-settings}. + ============================================== \ No newline at end of file diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 667038739d45f..94cca7f91494e 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -26,7 +26,7 @@ image::images/alert-flyout-general-details.png[alt='All alerts have name, tags, Name:: The name of the alert. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable alert listing in the management UI. A distinctive name can help identify and find an alert. Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. -Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. +Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. Notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. [float] @@ -59,7 +59,7 @@ Each action type exposes different properties. For example an email action allow [role="screenshot"] image::images/alert-flyout-action-details.png[UI for defining an email action] -Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Available variables differ by alert type, and a list can be accessed using the "add variable" button. +Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Note that using two curly braces will escape any HTML. Should you need to preserve HTML, use three curly braces (`{{{`). Available variables differ by alert type, and a list can be accessed using the "add variable" button. [role="screenshot"] image::images/alert-flyout-action-variables.png[Passing alert values to an action] diff --git a/docs/user/alerting/images/alert-concepts-summary.svg b/docs/user/alerting/images/alert-concepts-summary.svg index 0d63601c0693d..0aed3bf22375f 100644 --- a/docs/user/alerting/images/alert-concepts-summary.svg +++ b/docs/user/alerting/images/alert-concepts-summary.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 56404d9a33b80..caef0c6e7332d 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -2,4 +2,4 @@ include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] -include::alerting-scale-performance.asciidoc[] +include::alerting-production-considerations.asciidoc[] diff --git a/docs/user/dashboard/edit-dashboards.asciidoc b/docs/user/dashboard/edit-dashboards.asciidoc index 7b712b355b315..d7f7dc2d65c85 100644 --- a/docs/user/dashboard/edit-dashboards.asciidoc +++ b/docs/user/dashboard/edit-dashboards.asciidoc @@ -81,6 +81,21 @@ Put the dashboard in *Edit* mode, then use the following options: * To delete, open the panel menu, then select *Delete from dashboard*. When you delete a panel from the dashboard, the visualization or saved search from the panel is still available in Kibana. +[float] +[[sync-colors]] +=== Synchronize colors + +By default, dashboard panels that share a non-gradient based color palette will synchronize their color assignment to improve readability. +Color assignment is based on the series name, and the total number of colors is based on the number of unique series names. + +The color synchronizing logic can make the dashboard less readable when there are too many unique series names. It is possible to disable the synchronization behavior: + +. Put the dashboard in *Edit* mode. + +. Click the "Options" button in the top navigation bar. + +. Disable "Sync color palettes across panels". + [float] [[clone-panels]] === Clone panels diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index df9fa2dca81fd..b292c1ae5e03f 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -133,6 +133,12 @@ Example: `{{split event.value ","}}` +|encodeURIComponent +a|Escapes string using built in `encodeURIComponent` function. + +|encodeURIQuery +a|Escapes string using built in `encodeURIComponent` function, while keeping "@", ":", "$", ",", and ";" characters as is. + |=== diff --git a/docs/user/ml/images/ml-data-visualizer-sample.jpg b/docs/user/ml/images/ml-data-visualizer-sample.jpg index ce2bb660d7da1..4d77ef3010c3f 100644 Binary files a/docs/user/ml/images/ml-data-visualizer-sample.jpg and b/docs/user/ml/images/ml-data-visualizer-sample.jpg differ diff --git a/docs/user/reporting/response-codes.asciidoc b/docs/user/reporting/response-codes.asciidoc index 5ec9bf5300124..50a27766f37fb 100644 --- a/docs/user/reporting/response-codes.asciidoc +++ b/docs/user/reporting/response-codes.asciidoc @@ -9,7 +9,7 @@ the POST URL. This is true even if the job somehow fails later, since report generation happens asynchronously from queuing. - **`400` (Bad Request)**: When sending requests to the POST URL, if you don't use - `POST` as the HTTP method, or if your request is missing the `kbn-version` header, + `POST` as the HTTP method, or if your request is missing the `kbn-xsrf` header, Kibana will return a code `400` status response for the request. - **`503` (Service Unavailable)**: When using the `path` to request the download, you diff --git a/docs/user/reporting/script-example.asciidoc b/docs/user/reporting/script-example.asciidoc index 94301fc6fb448..56721d20ea3c7 100644 --- a/docs/user/reporting/script-example.asciidoc +++ b/docs/user/reporting/script-example.asciidoc @@ -3,7 +3,7 @@ The response from this request will be JSON, and will contain a `path` property URL to use to download the generated report. Use the `GET` method in the HTTP request to download the report. -The request method must be `POST` and it must include a `kbn-version` header for Kibana +The request method must be `POST` and it must include a `kbn-xsrf` header for Kibana to allow the request. The following example queues CSV report generation using the `POST` URL with cURL: @@ -13,7 +13,7 @@ The following example queues CSV report generation using the `POST` URL with cUR curl \ -XPOST \ <1> -u elastic \ <2> --H 'kbn-version: {version}' \ <3> +-H 'kbn-xsrf: true' \ <3> 'http://0.0.0.0:5601/api/reporting/generate/csv?jobParams=...' <4> --------------------------------------------------------- // CONSOLE @@ -21,8 +21,8 @@ curl \ <1> `POST` method is required. <2> Provide user credentials for a user with permission to access Kibana and {report-features}. -<3> The `kbn-version` header is required for all `POST` requests to Kibana. -**The value must match the dotted-numeral version of the Kibana instance.** +<3> The `kbn-xsrf` header is required for all `POST` requests to Kibana. For more information, see <>. <4> The POST URL. You can copy and paste the URL for any report from the Kibana UI. Here is an example response for a successfully queued report: diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc index 0509c6b13d54a..706c15fe6ec0f 100644 --- a/docs/user/security/securing-communications/index.asciidoc +++ b/docs/user/security/securing-communications/index.asciidoc @@ -26,65 +26,65 @@ NOTE: You do not need to enable the {es} {security-features} for this type of en When you obtain a server certificate, you must set its subject alternative name (SAN) correctly to ensure that modern web browsers with hostname verification will trust it. You can set one or more SANs to the {kib} server's fully-qualified domain name (FQDN), hostname, or IP address. When choosing the SAN, you should pick whichever attribute you will be using to connect to {kib} in your browser, which is likely -the FQDN. +the FQDN in a production environment. - -You may choose to generate a certificate signing request (CSR) and private key using the {ref}/certutil.html[`elasticsearch-certutil`] tool. +You may choose to generate a signed certificate and private key using the {ref}/certutil.html[`elasticsearch-certutil`] tool. For example: [source,sh] -------------------------------------------------------------------------------- -bin/elasticsearch-certutil csr -name kibana-server -dns some-website.com,www.some-website.com +bin/elasticsearch-certutil cert -name kibana-server -dns localhost,127.0.0.1 -------------------------------------------------------------------------------- -This will produce a ZIP archive named `kibana-server.zip`. Extract that archive to obtain the PEM-formatted CSR (`kibana-server.csr`) and -unencrypted private key (`kibana-server.key`). In this example, the CSR has a common name (CN) of `kibana-server`, a SAN of -`some-website.com`, and another SAN of `www.some-website.com`. +This will produce a PKCS#12 file named `kibana-server.p12`, which contains the server certificate and private key. -NOTE: You will need to use a certificate authority (CA) to sign your CSR to obtain your server certificate. This certificate's signature -will be verified by web browsers that are configured to trust the CA. +NOTE: In this example, the server certificate is signed by a locally-generated certificate authority (CA). This is not suitable for a +production environment, and it will result in warnings in your web browser until you configure your browser to trust the certificate. Steps +to configure certificate trust vary depending upon your browser and operating system. If you want to obtain a server certificate for a +production environment, you can instead generate a certificate signing request (CSR) with `elasticsearch-certutil` using +{ref}/certutil.html#certutil-csr[CSR mode]. -- . Configure {kib} to access the server certificate and private key. -.. If your server certificate and private key are in PEM format: +.. If your server certificate and private key are contained in a PKCS#12 file: + -- -Specify your server certificate and private key in `kibana.yml`: +Specify your PKCS#12 file in `kibana.yml`: [source,yaml] -------------------------------------------------------------------------------- -server.ssl.certificate: "/path/to/kibana-server.crt" -server.ssl.key: "/path/to/kibana-server.key" +server.ssl.keystore.path: "/path/to/kibana-server.p12" -------------------------------------------------------------------------------- -If your private key is encrypted, add the decryption password to your <>: +If your PKCS#12 file is encrypted, add the decryption password to your <>: [source,yaml] -------------------------------------------------------------------------------- -bin/kibana-keystore add server.ssl.keyPassphrase +bin/kibana-keystore add server.ssl.keystore.password -------------------------------------------------------------------------------- + +NOTE: If you used `elasticsearch-certutil` to generate a PKCS#12 file and you did not specify a password, the file is encrypted, and you +need to set `server.ssl.keystore.password` to an empty string. -- -.. Otherwise, if your server certificate and private key are contained in a PKCS#12 file: +.. Otherwise, if your server certificate and private key are in PEM format: + -- -Specify your PKCS#12 file in `kibana.yml`: +Specify your server certificate and private key in `kibana.yml`: [source,yaml] -------------------------------------------------------------------------------- -server.ssl.keystore.path: "/path/to/kibana-server.p12" +server.ssl.certificate: "/path/to/kibana-server.crt" +server.ssl.key: "/path/to/kibana-server.key" -------------------------------------------------------------------------------- -If your PKCS#12 file is encrypted, add the decryption password to your <>: +If your private key is encrypted, add the decryption password to your <>: [source,yaml] -------------------------------------------------------------------------------- -bin/kibana-keystore add server.ssl.keystore.password +bin/kibana-keystore add server.ssl.keyPassphrase -------------------------------------------------------------------------------- - -TIP: If your PKCS#12 file isn't protected with a password, depending on how it was generated, you may need to set -`server.ssl.keystore.password` to an empty string. -- + @@ -103,7 +103,7 @@ server.ssl.enabled: true . Restart {kib}. -After making these changes, you must always access {kib} via HTTPS. For example, https://.com. +After making these changes, you must always access {kib} via HTTPS. For example, `https://localhost:5601`. [[configuring-tls-kib-es]] ==== Encrypt traffic between {kib} and {es} @@ -166,8 +166,8 @@ If your PKCS#12 file is encrypted, add the decryption password to your <>. +NOTE: The password for the built-in `elastic` user is typically set as part of the security configuration process on {es}. For more +information, see {ref}/built-in-users.html[Built-in users]. -To manage privileges, open the main menu, then click *Stack Management > Roles*. +. [[kibana-roles]]Create roles and users to grant access to {kib}. ++ +-- +To manage privileges in {kib}, open the main menu, then click *Stack Management > Roles*. The built-in `kibana_admin` role will grant +access to {kib} with administrator privileges. Alternatively, you can create additional roles that grant limited access to {kib}. -If you're using the native realm with Basic Authentication, open then main menu, -then click *Stack Management > Users* to assign roles, or use the -{ref}/security-api.html#security-user-apis[user management APIs]. For example, -the following creates a user named `jacknich` and assigns it the `kibana_admin` -role: +If you're using the default native realm with Basic Authentication, open the main menu, then click *Stack Management > Users* to create +users and assign roles, or use the {es} {ref}/security-api.html#security-user-apis[user management APIs]. For example, the following creates +a user named `jacknich` and assigns it the `kibana_admin` role: [source,js] -------------------------------------------------------------------------------- @@ -98,6 +98,8 @@ POST /_security/user/jacknich } -------------------------------------------------------------------------------- // CONSOLE + +TIP: For more information on Basic Authentication and additional methods of authenticating {kib} users, see <>. -- . Grant users access to the indices that they will be working with in {kib}. @@ -111,17 +113,11 @@ on specific index patterns. For more information, see -- -. Verify that you can log in as a user. If you are running -{kib} locally, go to `https://localhost:5601` and enter the credentials for a -user you've assigned a {kib} user role. For example, you could log in as the user -`jacknich`. +. Log out of {kib} and verify that you can log in as a normal user. If you are running {kib} locally, go to `https://localhost:5601` and +enter the credentials for a user you've assigned a {kib} user role. For example, you could log in as the user `jacknich`. + --- - -NOTE: This must be a user who has been assigned <>. -{kib} server credentials should only be used internally by the {kib} server. - --- +NOTE: This must be a user who has been assigned <>. {kib} server credentials (the built-in +`kibana_system` user) should only be used internally by the {kib} server. include::authentication/index.asciidoc[] include::securing-communications/index.asciidoc[] diff --git a/examples/embeddable_examples/public/book/add_book_to_library_action.tsx b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx index 4ae3a545df0d0..b36635feb3dcc 100644 --- a/examples/embeddable_examples/public/book/add_book_to_library_action.tsx +++ b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx @@ -35,6 +35,7 @@ export const createAddBookToLibraryAction = () => i18n.translate('embeddableExamples.book.addToLibrary', { defaultMessage: 'Add Book To Library', }), + id: ACTION_ADD_BOOK_TO_LIBRARY, type: ACTION_ADD_BOOK_TO_LIBRARY, order: 100, getIconType: () => 'folderCheck', diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 877e50560000e..6fa5ff15716a6 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -53,6 +53,7 @@ export const createEditBookAction = (getStartServices: () => Promise i18n.translate('embeddableExamples.book.edit', { defaultMessage: 'Edit Book' }), + id: ACTION_EDIT_BOOK, type: ACTION_EDIT_BOOK, order: 100, getIconType: () => 'documents', diff --git a/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx index ebab2c483c625..54857010a1468 100644 --- a/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx +++ b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx @@ -35,6 +35,7 @@ export const createUnlinkBookFromLibraryAction = () => i18n.translate('embeddableExamples.book.unlinkFromLibrary', { defaultMessage: 'Unlink Book from Library Item', }), + id: ACTION_UNLINK_BOOK_FROM_LIBRARY, type: ACTION_UNLINK_BOOK_FROM_LIBRARY, order: 100, getIconType: () => 'folderExclamation', diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 9b9770e40611e..8e8f2ddfe2128 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -51,21 +51,15 @@ import { TodoRefEmbeddableFactory, TodoRefEmbeddableFactoryDefinition, } from './todo/todo_ref_embeddable_factory'; -import { ACTION_EDIT_BOOK, createEditBookAction } from './book/edit_book_action'; -import { BookEmbeddable, BOOK_EMBEDDABLE } from './book/book_embeddable'; +import { createEditBookAction } from './book/edit_book_action'; +import { BOOK_EMBEDDABLE } from './book/book_embeddable'; import { BookEmbeddableFactory, BookEmbeddableFactoryDefinition, } from './book/book_embeddable_factory'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; -import { - ACTION_ADD_BOOK_TO_LIBRARY, - createAddBookToLibraryAction, -} from './book/add_book_to_library_action'; -import { - ACTION_UNLINK_BOOK_FROM_LIBRARY, - createUnlinkBookFromLibraryAction, -} from './book/unlink_book_from_library_action'; +import { createAddBookToLibraryAction } from './book/add_book_to_library_action'; +import { createUnlinkBookFromLibraryAction } from './book/unlink_book_from_library_action'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; @@ -92,14 +86,6 @@ export interface EmbeddableExamplesStart { factories: ExampleEmbeddableFactories; } -declare module '../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable }; - [ACTION_ADD_BOOK_TO_LIBRARY]: { embeddable: BookEmbeddable }; - [ACTION_UNLINK_BOOK_FROM_LIBRARY]: { embeddable: BookEmbeddable }; - } -} - export class EmbeddableExamplesPlugin implements Plugin< diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index 33ad8bbfe3d35..afdcc8d4a8bd6 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -295,7 +295,7 @@ export const SearchExamplesApp = ({ Index Pattern Promise) => createAction({ + id: ACTION_HELLO_WORLD, type: ACTION_HELLO_WORLD, getDisplayName: () => 'Hello World!', execute: async () => { diff --git a/examples/ui_action_examples/public/plugin.ts b/examples/ui_action_examples/public/plugin.ts index 3a9f673261e33..1d896d3305661 100644 --- a/examples/ui_action_examples/public/plugin.ts +++ b/examples/ui_action_examples/public/plugin.ts @@ -19,8 +19,8 @@ import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public'; -import { createHelloWorldAction, ACTION_HELLO_WORLD } from './hello_world_action'; -import { helloWorldTrigger, HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; +import { createHelloWorldAction } from './hello_world_action'; +import { helloWorldTrigger } from './hello_world_trigger'; export interface UiActionExamplesSetupDependencies { uiActions: UiActionsSetup; @@ -30,16 +30,6 @@ export interface UiActionExamplesStartDependencies { uiActions: UiActionsStart; } -declare module '../../../src/plugins/ui_actions/public' { - export interface TriggerContextMapping { - [HELLO_WORLD_TRIGGER_ID]: {}; - } - - export interface ActionContextMapping { - [ACTION_HELLO_WORLD]: {}; - } -} - export class UiActionExamplesPlugin implements Plugin { diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index 777bcd9c18119..1d9987ad73a26 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -38,7 +38,8 @@ export const ACTION_EDIT_USER = 'ACTION_EDIT_USER'; export const ACTION_TRIGGER_PHONE_USER = 'ACTION_TRIGGER_PHONE_USER'; export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY'; -export const showcasePluggability = createAction({ +export const showcasePluggability = createAction({ + id: ACTION_SHOWCASE_PLUGGABILITY, type: ACTION_SHOWCASE_PLUGGABILITY, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', execute: async (context: ActionExecutionContext) => @@ -49,13 +50,15 @@ export interface PhoneContext { phone: string; } -export const makePhoneCallAction = createAction({ +export const makePhoneCallAction = createAction({ + id: ACTION_CALL_PHONE_NUMBER, type: ACTION_CALL_PHONE_NUMBER, getDisplayName: () => 'Call phone number', execute: async (context) => alert(`Pretend calling ${context.phone}...`), }); -export const lookUpWeatherAction = createAction({ +export const lookUpWeatherAction = createAction({ + id: ACTION_TRAVEL_GUIDE, type: ACTION_TRAVEL_GUIDE, getIconType: () => 'popout', getDisplayName: () => 'View travel guide', @@ -68,7 +71,8 @@ export interface CountryContext { country: string; } -export const viewInMapsAction = createAction({ +export const viewInMapsAction = createAction({ + id: ACTION_VIEW_IN_MAPS, type: ACTION_VIEW_IN_MAPS, getIconType: () => 'popout', getDisplayName: () => 'View in maps', @@ -109,7 +113,8 @@ function EditUserModal({ } export const createEditUserAction = (getOpenModal: () => Promise) => - createAction({ + createAction({ + id: ACTION_EDIT_USER, type: ACTION_EDIT_USER, getIconType: () => 'pencil', getDisplayName: () => 'Edit user', @@ -126,7 +131,8 @@ export interface UserContext { } export const createTriggerPhoneTriggerAction = (getUiActionsApi: () => Promise) => - createAction({ + createAction({ + id: ACTION_TRIGGER_PHONE_USER, type: ACTION_TRIGGER_PHONE_USER, getDisplayName: () => 'Call phone number', shouldAutoExecute: async () => true, diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index bc8bdee75047d..38fc4e22b284f 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -77,7 +77,7 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { { - const dynamicAction = createAction({ + const dynamicAction = createAction({ id: `${ACTION_HELLO_WORLD}-${name}`, type: ACTION_HELLO_WORLD, getDisplayName: () => `Say hello to ${name}`, diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index b28e5e7a9f692..757a2f1dfa865 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -28,15 +28,6 @@ import { createEditUserAction, makePhoneCallAction, showcasePluggability, - UserContext, - CountryContext, - PhoneContext, - ACTION_EDIT_USER, - ACTION_SHOWCASE_PLUGGABILITY, - ACTION_CALL_PHONE_NUMBER, - ACTION_TRAVEL_GUIDE, - ACTION_VIEW_IN_MAPS, - ACTION_TRIGGER_PHONE_USER, createTriggerPhoneTriggerAction, } from './actions/actions'; import { DeveloperExamplesSetup } from '../../developer_examples/public'; @@ -51,23 +42,6 @@ interface SetupDeps { developerExamples: DeveloperExamplesSetup; } -declare module '../../../src/plugins/ui_actions/public' { - export interface TriggerContextMapping { - [USER_TRIGGER]: UserContext; - [COUNTRY_TRIGGER]: CountryContext; - [PHONE_TRIGGER]: PhoneContext; - } - - export interface ActionContextMapping { - [ACTION_EDIT_USER]: UserContext; - [ACTION_SHOWCASE_PLUGGABILITY]: {}; - [ACTION_CALL_PHONE_NUMBER]: PhoneContext; - [ACTION_TRAVEL_GUIDE]: CountryContext; - [ACTION_VIEW_IN_MAPS]: CountryContext; - [ACTION_TRIGGER_PHONE_USER]: UserContext; - } -} - export class UiActionsExplorerPlugin implements Plugin { public setup(core: CoreSetup, deps: SetupDeps) { deps.uiActions.registerTrigger({ diff --git a/package.json b/package.json index 6e8809063ca57..8af20d6459373 100644 --- a/package.json +++ b/package.json @@ -93,16 +93,16 @@ "**/typescript": "4.1.2" }, "engines": { - "node": "14.15.3", + "node": "14.15.4", "yarn": "^1.21.1" }, "dependencies": { "@babel/core": "^7.11.6", "@babel/runtime": "^7.11.2", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "7.10.0", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", "@elastic/ems-client": "7.11.0", - "@elastic/eui": "30.6.0", + "@elastic/eui": "31.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", @@ -116,7 +116,7 @@ "@hapi/good-squeeze": "6.0.0", "@hapi/h2o2": "^9.0.2", "@hapi/hapi": "^20.0.3", - "@hapi/hoek": "^9.1.0", + "@hapi/hoek": "^9.1.1", "@hapi/inert": "^6.0.3", "@hapi/podium": "^4.1.1", "@hapi/statehood": "^7.0.3", @@ -140,7 +140,16 @@ "@loaders.gl/json": "^2.3.1", "@slack/webhook": "^5.0.0", "@storybook/addons": "^6.0.16", + "@turf/along": "6.0.1", + "@turf/area": "6.0.1", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", + "@turf/boolean-contains": "6.0.1", + "@turf/center-of-mass": "6.0.1", "@turf/circle": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", + "@turf/length": "^6.0.2", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.4.0", @@ -223,6 +232,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^2.1.0", "io-ts": "^2.0.5", + "ipaddr.js": "2.0.0", "isbinaryfile": "4.0.2", "joi": "^13.5.2", "jquery": "^3.5.0", @@ -264,7 +274,7 @@ "proper-lockfile": "^3.2.0", "proxy-from-env": "1.0.0", "puid": "1.0.7", - "puppeteer": "^5.5.0", + "puppeteer": "npm:@elastic/puppeteer@5.4.1-patch.1", "query-string": "^6.13.2", "raw-loader": "^3.1.0", "re2": "^1.15.4", @@ -343,7 +353,7 @@ "@babel/traverse": "^7.11.5", "@babel/types": "^7.11.0", "@cypress/snapshot": "^2.1.7", - "@cypress/webpack-preprocessor": "^5.4.11", + "@cypress/webpack-preprocessor": "^5.5.0", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", "@elastic/charts": "24.4.0", @@ -399,11 +409,6 @@ "@testing-library/react": "^11.0.4", "@testing-library/react-hooks": "^3.4.1", "@testing-library/user-event": "^12.1.6", - "@turf/bbox": "6.0.1", - "@turf/bbox-polygon": "6.0.1", - "@turf/boolean-contains": "6.0.1", - "@turf/distance": "6.0.1", - "@turf/helpers": "6.0.1", "@types/accept": "3.1.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", @@ -603,7 +608,7 @@ "cpy": "^8.1.1", "cronstrue": "^1.51.0", "css-loader": "^3.4.2", - "cypress": "^6.1.0", + "cypress": "^6.2.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "d3": "3.5.17", @@ -824,10 +829,10 @@ "url-loader": "^2.2.0", "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.17.0", + "vega": "^5.17.3", "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", - "vega-tooltip": "^0.24.2", + "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", "wait-on": "^5.0.1", diff --git a/packages/elastic-datemath/__tests__/index.js b/packages/elastic-datemath/index.test.js similarity index 69% rename from packages/elastic-datemath/__tests__/index.js rename to packages/elastic-datemath/index.test.js index 1a61021b48a6e..a6a576dd713a9 100644 --- a/packages/elastic-datemath/__tests__/index.js +++ b/packages/elastic-datemath/index.test.js @@ -17,22 +17,21 @@ * under the License. */ -const dateMath = require('../index'); +const dateMath = require('./index'); const moment = require('moment'); -const sinon = require('sinon'); -const expect = require('@kbn/expect'); /** - * Require a new instance of the moment library, bypassing the require cache. + * Require a new instance of the moment library, bypassing the require cache + * by using jest.resetModules(). * This is needed, since we are trying to test whether or not this library works * when passing in a different configured moment instance. If we would change * the locales on the imported moment, it would automatically apply * to the source code, even without passing it in to the method, since they share * the same global state. This method avoids this, by loading a separate instance - * of moment, by deleting the require cache and require the library again. + * of moment, by resetting the jest require modules cache and require the library again. */ function momentClone() { - delete require.cache[require.resolve('moment')]; + jest.resetModules(); return require('moment'); } @@ -43,44 +42,43 @@ describe('dateMath', function () { const anchoredDate = new Date(Date.parse(anchor)); const unix = moment(anchor).valueOf(); const format = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; - let clock; describe('errors', function () { it('should return undefined if passed something falsy', function () { - expect(dateMath.parse()).to.be(undefined); + expect(dateMath.parse()).toBeUndefined(); }); it('should return undefined if I pass an operator besides [+-/]', function () { - expect(dateMath.parse('now&1d')).to.be(undefined); + expect(dateMath.parse('now&1d')).toBeUndefined(); }); it('should return undefined if I pass a unit besides' + spans.toString(), function () { - expect(dateMath.parse('now+5f')).to.be(undefined); + expect(dateMath.parse('now+5f')).toBeUndefined(); }); it('should return undefined if rounding unit is not 1', function () { - expect(dateMath.parse('now/2y')).to.be(undefined); - expect(dateMath.parse('now/0.5y')).to.be(undefined); + expect(dateMath.parse('now/2y')).toBeUndefined(); + expect(dateMath.parse('now/0.5y')).toBeUndefined(); }); it('should not go into an infinite loop when missing a unit', function () { - expect(dateMath.parse('now-0')).to.be(undefined); - expect(dateMath.parse('now-00')).to.be(undefined); - expect(dateMath.parse('now-000')).to.be(undefined); + expect(dateMath.parse('now-0')).toBeUndefined(); + expect(dateMath.parse('now-00')).toBeUndefined(); + expect(dateMath.parse('now-000')).toBeUndefined(); }); describe('forceNow', function () { it('should throw an Error if passed a string', function () { const fn = () => dateMath.parse('now', { forceNow: '2000-01-01T00:00:00.000Z' }); - expect(fn).to.throwError(); + expect(fn).toThrowError(); }); it('should throw an Error if passed a moment', function () { - expect(() => dateMath.parse('now', { forceNow: moment() })).to.throwError(); + expect(() => dateMath.parse('now', { forceNow: moment() })).toThrowError(); }); it('should throw an Error if passed an invalid date', function () { - expect(() => dateMath.parse('now', { forceNow: new Date('foobar') })).to.throwError(); + expect(() => dateMath.parse('now', { forceNow: new Date('foobar') })).toThrowError(); }); }); }); @@ -92,7 +90,8 @@ describe('dateMath', function () { let now; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); mmnt = moment(anchor); date = mmnt.toDate(); @@ -100,27 +99,27 @@ describe('dateMath', function () { }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); it('should return the same moment if passed a moment', function () { - expect(dateMath.parse(mmnt)).to.eql(mmnt); + expect(dateMath.parse(mmnt)).toEqual(mmnt); }); it('should return a moment if passed a date', function () { - expect(dateMath.parse(date).format(format)).to.eql(mmnt.format(format)); + expect(dateMath.parse(date).format(format)).toEqual(mmnt.format(format)); }); it('should return a moment if passed an ISO8601 string', function () { - expect(dateMath.parse(string).format(format)).to.eql(mmnt.format(format)); + expect(dateMath.parse(string).format(format)).toEqual(mmnt.format(format)); }); it('should return the current time when parsing now', function () { - expect(dateMath.parse('now').format(format)).to.eql(now.format(format)); + expect(dateMath.parse('now').format(format)).toEqual(now.format(format)); }); it('should use the forceNow parameter when parsing now', function () { - expect(dateMath.parse('now', { forceNow: anchoredDate }).valueOf()).to.eql(unix); + expect(dateMath.parse('now', { forceNow: anchoredDate }).valueOf()).toEqual(unix); }); }); @@ -129,13 +128,14 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); [5, 12, 247].forEach((len) => { @@ -145,17 +145,17 @@ describe('dateMath', function () { it('should return ' + len + span + ' ago', function () { const parsed = dateMath.parse(nowEx).format(format); - expect(parsed).to.eql(now.subtract(len, span).format(format)); + expect(parsed).toEqual(now.subtract(len, span).format(format)); }); it('should return ' + len + span + ' before ' + anchor, function () { const parsed = dateMath.parse(thenEx).format(format); - expect(parsed).to.eql(anchored.subtract(len, span).format(format)); + expect(parsed).toEqual(anchored.subtract(len, span).format(format)); }); it('should return ' + len + span + ' before forceNow', function () { const parsed = dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf(); - expect(parsed).to.eql(anchored.subtract(len, span).valueOf()); + expect(parsed).toEqual(anchored.subtract(len, span).valueOf()); }); }); }); @@ -166,13 +166,14 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); [5, 12, 247].forEach((len) => { @@ -181,17 +182,17 @@ describe('dateMath', function () { const thenEx = `${anchor}||+${len}${span}`; it('should return ' + len + span + ' from now', function () { - expect(dateMath.parse(nowEx).format(format)).to.eql(now.add(len, span).format(format)); + expect(dateMath.parse(nowEx).format(format)).toEqual(now.add(len, span).format(format)); }); it('should return ' + len + span + ' after ' + anchor, function () { - expect(dateMath.parse(thenEx).format(format)).to.eql( + expect(dateMath.parse(thenEx).format(format)).toEqual( anchored.add(len, span).format(format) ); }); it('should return ' + len + span + ' after forceNow', function () { - expect(dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf()).to.eql( + expect(dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf()).toEqual( anchored.add(len, span).valueOf() ); }); @@ -204,30 +205,31 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); spans.forEach((span) => { it(`should round now to the beginning of the ${span}`, function () { - expect(dateMath.parse('now/' + span).format(format)).to.eql( + expect(dateMath.parse('now/' + span).format(format)).toEqual( now.startOf(span).format(format) ); }); it(`should round now to the beginning of forceNow's ${span}`, function () { - expect(dateMath.parse('now/' + span, { forceNow: anchoredDate }).valueOf()).to.eql( + expect(dateMath.parse('now/' + span, { forceNow: anchoredDate }).valueOf()).toEqual( anchored.startOf(span).valueOf() ); }); it(`should round now to the end of the ${span}`, function () { - expect(dateMath.parse('now/' + span, { roundUp: true }).format(format)).to.eql( + expect(dateMath.parse('now/' + span, { roundUp: true }).format(format)).toEqual( now.endOf(span).format(format) ); }); @@ -235,7 +237,7 @@ describe('dateMath', function () { it(`should round now to the end of forceNow's ${span}`, function () { expect( dateMath.parse('now/' + span, { roundUp: true, forceNow: anchoredDate }).valueOf() - ).to.eql(anchored.endOf(span).valueOf()); + ).toEqual(anchored.endOf(span).valueOf()); }); }); }); @@ -245,38 +247,39 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); it('should round to the nearest second with 0 value', function () { const val = dateMath.parse('now-0s/s').format(format); - expect(val).to.eql(now.startOf('s').format(format)); + expect(val).toEqual(now.startOf('s').format(format)); }); it('should subtract 17s, rounded to the nearest second', function () { const val = dateMath.parse('now-17s/s').format(format); - expect(val).to.eql(now.startOf('s').subtract(17, 's').format(format)); + expect(val).toEqual(now.startOf('s').subtract(17, 's').format(format)); }); it('should add 555ms, rounded to the nearest millisecond', function () { const val = dateMath.parse('now+555ms/ms').format(format); - expect(val).to.eql(now.add(555, 'ms').startOf('ms').format(format)); + expect(val).toEqual(now.add(555, 'ms').startOf('ms').format(format)); }); it('should subtract 555ms, rounded to the nearest second', function () { const val = dateMath.parse('now-555ms/s').format(format); - expect(val).to.eql(now.subtract(555, 'ms').startOf('s').format(format)); + expect(val).toEqual(now.subtract(555, 'ms').startOf('s').format(format)); }); it('should round weeks to Sunday by default', function () { const val = dateMath.parse('now-1w/w'); - expect(val.isoWeekday()).to.eql(7); + expect(val.isoWeekday()).toEqual(7); }); it('should round weeks based on the passed moment locale start of week setting', function () { @@ -286,7 +289,7 @@ describe('dateMath', function () { week: { dow: 2 }, }); const val = dateMath.parse('now-1w/w', { momentInstance: m }); - expect(val.isoWeekday()).to.eql(2); + expect(val.isoWeekday()).toEqual(2); }); it('should round up weeks based on the passed moment locale start of week setting', function () { @@ -301,79 +304,79 @@ describe('dateMath', function () { }); // The end of the range (rounding up) should be the last day of the week (so one day before) // our start of the week, that's why 3 - 1 - expect(val.isoWeekday()).to.eql(3 - 1); + expect(val.isoWeekday()).toEqual(3 - 1); }); it('should round relative to forceNow', function () { const val = dateMath.parse('now-0s/s', { forceNow: anchoredDate }).valueOf(); - expect(val).to.eql(anchored.startOf('s').valueOf()); + expect(val).toEqual(anchored.startOf('s').valueOf()); }); it('should parse long expressions', () => { - expect(dateMath.parse('now-1d/d+8h+50m')).to.be.ok(); + expect(dateMath.parse('now-1d/d+8h+50m')).toBeTruthy(); }); }); describe('used momentjs instance', function () { it('should use the default moment instance if parameter not specified', function () { - const momentSpy = sinon.spy(moment, 'isMoment'); + const momentSpy = jest.spyOn(moment, 'isMoment'); dateMath.parse('now'); - expect(momentSpy.called).to.be(true); - momentSpy.restore(); + expect(momentSpy).toHaveBeenCalled(); + momentSpy.mockRestore(); }); it('should not use default moment instance if parameter is specified', function () { const m = momentClone(); - const momentSpy = sinon.spy(moment, 'isMoment'); - const cloneSpy = sinon.spy(m, 'isMoment'); + const momentSpy = jest.spyOn(moment, 'isMoment'); + const cloneSpy = jest.spyOn(m, 'isMoment'); dateMath.parse('now', { momentInstance: m }); - expect(momentSpy.called).to.be(false); - expect(cloneSpy.called).to.be(true); - momentSpy.restore(); - cloneSpy.restore(); + expect(momentSpy).not.toHaveBeenCalled(); + expect(cloneSpy).toHaveBeenCalled(); + momentSpy.mockRestore(); + cloneSpy.mockRestore(); }); it('should work with multiple different instances', function () { const m1 = momentClone(); const m2 = momentClone(); - const m1Spy = sinon.spy(m1, 'isMoment'); - const m2Spy = sinon.spy(m2, 'isMoment'); + const m1Spy = jest.spyOn(m1, 'isMoment'); + const m2Spy = jest.spyOn(m2, 'isMoment'); dateMath.parse('now', { momentInstance: m1 }); - expect(m1Spy.called).to.be(true); - expect(m2Spy.called).to.be(false); - m1Spy.resetHistory(); - m2Spy.resetHistory(); + expect(m1Spy).toHaveBeenCalled(); + expect(m2Spy).not.toHaveBeenCalled(); + m1Spy.mockClear(); + m2Spy.mockClear(); dateMath.parse('now', { momentInstance: m2 }); - expect(m1Spy.called).to.be(false); - expect(m2Spy.called).to.be(true); - m1Spy.restore(); - m2Spy.restore(); + expect(m1Spy).not.toHaveBeenCalled(); + expect(m2Spy).toHaveBeenCalled(); + m1Spy.mockRestore(); + m2Spy.mockRestore(); }); it('should use global instance after passing an instance', function () { const m = momentClone(); - const momentSpy = sinon.spy(moment, 'isMoment'); - const cloneSpy = sinon.spy(m, 'isMoment'); + const momentSpy = jest.spyOn(moment, 'isMoment'); + const cloneSpy = jest.spyOn(m, 'isMoment'); dateMath.parse('now', { momentInstance: m }); - expect(momentSpy.called).to.be(false); - expect(cloneSpy.called).to.be(true); - momentSpy.resetHistory(); - cloneSpy.resetHistory(); + expect(momentSpy).not.toHaveBeenCalled(); + expect(cloneSpy).toHaveBeenCalled(); + momentSpy.mockClear(); + cloneSpy.mockClear(); dateMath.parse('now'); - expect(momentSpy.called).to.be(true); - expect(cloneSpy.called).to.be(false); - momentSpy.restore(); - cloneSpy.restore(); + expect(momentSpy).toHaveBeenCalled(); + expect(cloneSpy).not.toHaveBeenCalled(); + momentSpy.mockRestore(); + cloneSpy.mockRestore(); }); }); describe('units', function () { it('should have units descending for unitsDesc', function () { - expect(dateMath.unitsDesc).to.eql(['y', 'M', 'w', 'd', 'h', 'm', 's', 'ms']); + expect(dateMath.unitsDesc).toEqual(['y', 'M', 'w', 'd', 'h', 'm', 's', 'ms']); }); it('should have units ascending for unitsAsc', function () { - expect(dateMath.unitsAsc).to.eql(['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y']); + expect(dateMath.unitsAsc).toEqual(['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y']); }); }); }); diff --git a/packages/elastic-datemath/jest.config.js b/packages/elastic-datemath/jest.config.js new file mode 100644 index 0000000000000..6a0bd891f1c58 --- /dev/null +++ b/packages/elastic-datemath/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/elastic-datemath'], + testEnvironment: 'jsdom', +}; diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index 6e5a830d04b17..5e3d52cfd27d1 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -153,8 +153,8 @@ export class ApmConfiguration { return { globalLabels: { - branch: process.env.ghprbSourceBranch || '', - targetBranch: process.env.ghprbTargetBranch || '', + branch: process.env.GIT_BRANCH || '', + targetBranch: process.env.PR_TARGET_BRANCH || '', ciBuildNumber: process.env.BUILD_NUMBER || '', isPr: process.env.GITHUB_PR_NUMBER ? true : false, prId: process.env.GITHUB_PR_NUMBER || '', diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index d61d544deadc4..5f4e37ee35edf 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -20,7 +20,7 @@ import { Client } from 'elasticsearch'; import { ToolingLog, KbnClient } from '@kbn/dev-utils'; -import { migrateKibanaIndex, deleteKibanaIndices, createStats } from '../lib'; +import { migrateKibanaIndex, createStats, cleanKibanaIndices } from '../lib'; export async function emptyKibanaIndexAction({ client, @@ -32,8 +32,9 @@ export async function emptyKibanaIndexAction({ kbnClient: KbnClient; }) { const stats = createStats('emptyKibanaIndex', log); + const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); - await deleteKibanaIndices({ client, stats, log }); + await cleanKibanaIndices({ client, stats, log, kibanaPluginIds }); await migrateKibanaIndex({ client, kbnClient }); return stats; } diff --git a/packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts b/packages/kbn-es-archiver/src/lib/archives/format.test.ts similarity index 80% rename from packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts rename to packages/kbn-es-archiver/src/lib/archives/format.test.ts index 91c38d0dd1438..5190ea0128173 100644 --- a/packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts +++ b/packages/kbn-es-archiver/src/lib/archives/format.test.ts @@ -20,10 +20,9 @@ import Stream, { Readable, Writable } from 'stream'; import { createGunzip } from 'zlib'; -import expect from '@kbn/expect'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createFormatArchiveStreams } from '../format'; +import { createFormatArchiveStreams } from './format'; const INPUTS = [1, 2, { foo: 'bar' }, [1, 2]]; const INPUT_JSON = INPUTS.map((i) => JSON.stringify(i, null, 2)).join('\n\n'); @@ -32,9 +31,9 @@ describe('esArchiver createFormatArchiveStreams', () => { describe('{ gzip: false }', () => { it('returns an array of streams', () => { const streams = createFormatArchiveStreams({ gzip: false }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); it('streams consume js values and produces buffers', async () => { @@ -44,8 +43,8 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output.length).to.be.greaterThan(0); - output.forEach((b) => expect(b).to.be.a(Buffer)); + expect(output.length).toBeGreaterThan(0); + output.forEach((b) => expect(b).toBeInstanceOf(Buffer)); }); it('product is pretty-printed JSON separated by two newlines', async () => { @@ -55,16 +54,16 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream(''), ] as [Readable, ...Writable[]]); - expect(json).to.be(INPUT_JSON); + expect(json).toBe(INPUT_JSON); }); }); describe('{ gzip: true }', () => { it('returns an array of streams', () => { const streams = createFormatArchiveStreams({ gzip: true }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); it('streams consume js values and produces buffers', async () => { @@ -74,8 +73,8 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output.length).to.be.greaterThan(0); - output.forEach((b) => expect(b).to.be.a(Buffer)); + expect(output.length).toBeGreaterThan(0); + output.forEach((b) => expect(b).toBeInstanceOf(Buffer)); }); it('output can be gunzipped', async () => { @@ -85,7 +84,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createGunzip(), createConcatStream(''), ] as [Readable, ...Writable[]]); - expect(output).to.be(INPUT_JSON); + expect(output).toBe(INPUT_JSON); }); }); @@ -97,7 +96,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream(''), ] as [Readable, ...Writable[]]); - expect(json).to.be(INPUT_JSON); + expect(json).toBe(INPUT_JSON); }); }); }); diff --git a/packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts b/packages/kbn-es-archiver/src/lib/archives/parse.test.ts similarity index 85% rename from packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts rename to packages/kbn-es-archiver/src/lib/archives/parse.test.ts index deaea5cd4532e..70be5308ddfd4 100644 --- a/packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts +++ b/packages/kbn-es-archiver/src/lib/archives/parse.test.ts @@ -20,18 +20,17 @@ import Stream, { PassThrough, Readable, Writable, Transform } from 'stream'; import { createGzip } from 'zlib'; -import expect from '@kbn/expect'; import { createConcatStream, createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { createParseArchiveStreams } from '../parse'; +import { createParseArchiveStreams } from './parse'; describe('esArchiver createParseArchiveStreams', () => { describe('{ gzip: false }', () => { it('returns an array of streams', () => { const streams = createParseArchiveStreams({ gzip: false }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); describe('streams', () => { @@ -46,7 +45,7 @@ describe('esArchiver createParseArchiveStreams', () => { ...createParseArchiveStreams({ gzip: false }), ]); - expect(output).to.eql({ a: 1 }); + expect(output).toEqual({ a: 1 }); }); it('consume buffers of valid JSON separated by two newlines', async () => { const output = await createPromiseFromStreams([ @@ -63,7 +62,7 @@ describe('esArchiver createParseArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output).to.eql([{ a: 1 }, 1]); + expect(output).toEqual([{ a: 1 }, 1]); }); it('provides each JSON object as soon as it is parsed', async () => { @@ -87,10 +86,10 @@ describe('esArchiver createParseArchiveStreams', () => { ] as [Readable, ...Writable[]]); input.write(Buffer.from('{"a": 1}\n\n{"a":')); - expect(await receivedPromise).to.eql({ a: 1 }); + expect(await receivedPromise).toEqual({ a: 1 }); input.write(Buffer.from('2}')); input.end(); - expect(await finalPromise).to.eql([{ a: 1 }, { a: 2 }]); + expect(await finalPromise).toEqual([{ a: 1 }, { a: 2 }]); }); }); @@ -108,7 +107,7 @@ describe('esArchiver createParseArchiveStreams', () => { ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { - expect(err.message).to.contain('Unexpected number'); + expect(err.message).toEqual(expect.stringContaining('Unexpected number')); } }); }); @@ -117,9 +116,9 @@ describe('esArchiver createParseArchiveStreams', () => { describe('{ gzip: true }', () => { it('returns an array of streams', () => { const streams = createParseArchiveStreams({ gzip: true }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); describe('streams', () => { @@ -135,7 +134,7 @@ describe('esArchiver createParseArchiveStreams', () => { ...createParseArchiveStreams({ gzip: true }), ]); - expect(output).to.eql({ a: 1 }); + expect(output).toEqual({ a: 1 }); }); it('parses valid gzipped JSON strings separated by two newlines', async () => { @@ -146,7 +145,7 @@ describe('esArchiver createParseArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output).to.eql([{ a: 1 }, { a: 2 }]); + expect(output).toEqual([{ a: 1 }, { a: 2 }]); }); }); @@ -158,7 +157,7 @@ describe('esArchiver createParseArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output).to.eql([]); + expect(output).toEqual([]); }); describe('stream errors', () => { @@ -171,7 +170,7 @@ describe('esArchiver createParseArchiveStreams', () => { ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { - expect(err.message).to.contain('incorrect header check'); + expect(err.message).toEqual(expect.stringContaining('incorrect header check')); } }); }); @@ -183,7 +182,7 @@ describe('esArchiver createParseArchiveStreams', () => { createListStream([Buffer.from('{"a": 1}')]), ...createParseArchiveStreams(), ]); - expect(output).to.eql({ a: 1 }); + expect(output).toEqual({ a: 1 }); }); }); }); diff --git a/packages/kbn-es-archiver/src/lib/docs/__tests__/stubs.ts b/packages/kbn-es-archiver/src/lib/docs/__mocks__/stubs.ts similarity index 100% rename from packages/kbn-es-archiver/src/lib/docs/__tests__/stubs.ts rename to packages/kbn-es-archiver/src/lib/docs/__mocks__/stubs.ts diff --git a/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts similarity index 71% rename from packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts rename to packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts index 074333eb6028f..dad6008c89824 100644 --- a/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts @@ -18,22 +18,21 @@ */ import sinon from 'sinon'; -import expect from '@kbn/expect'; import { delay } from 'bluebird'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createGenerateDocRecordsStream } from '../generate_doc_records_stream'; -import { Progress } from '../../progress'; -import { createStubStats, createStubClient } from './stubs'; +import { createGenerateDocRecordsStream } from './generate_doc_records_stream'; +import { Progress } from '../progress'; +import { createStubStats, createStubClient } from './__mocks__/stubs'; describe('esArchiver: createGenerateDocRecordsStream()', () => { it('scolls 1000 documents at a time', async () => { const stats = createStubStats(); const client = createStubClient([ (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'logstash-*'); - expect(params).to.have.property('size', 1000); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'logstash-*'); + expect(params).toHaveProperty('size', 1000); return { hits: { total: 0, @@ -49,18 +48,18 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { createGenerateDocRecordsStream({ client, stats, progress }), ]); - expect(progress.getTotal()).to.be(0); - expect(progress.getComplete()).to.be(0); + expect(progress.getTotal()).toBe(0); + expect(progress.getComplete()).toBe(0); }); it('uses a 1 minute scroll timeout', async () => { const stats = createStubStats(); const client = createStubClient([ (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'logstash-*'); - expect(params).to.have.property('scroll', '1m'); - expect(params).to.have.property('rest_total_hits_as_int', true); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'logstash-*'); + expect(params).toHaveProperty('scroll', '1m'); + expect(params).toHaveProperty('rest_total_hits_as_int', true); return { hits: { total: 0, @@ -76,8 +75,8 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { createGenerateDocRecordsStream({ client, stats, progress }), ]); - expect(progress.getTotal()).to.be(0); - expect(progress.getComplete()).to.be(0); + expect(progress.getTotal()).toBe(0); + expect(progress.getComplete()).toBe(0); }); it('consumes index names and scrolls completely before continuing', async () => { @@ -85,8 +84,8 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { let checkpoint = Date.now(); const client = createStubClient([ async (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'index1'); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'index1'); await delay(200); return { _scroll_id: 'index1ScrollId', @@ -94,17 +93,17 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }; }, async (name, params) => { - expect(name).to.be('scroll'); - expect(params).to.have.property('scrollId', 'index1ScrollId'); - expect(Date.now() - checkpoint).to.not.be.lessThan(200); + expect(name).toBe('scroll'); + expect(params).toHaveProperty('scrollId', 'index1ScrollId'); + expect(Date.now() - checkpoint).not.toBeLessThan(200); checkpoint = Date.now(); await delay(200); return { hits: { total: 2, hits: [{ _id: 2, _index: 'foo' }] } }; }, async (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'index2'); - expect(Date.now() - checkpoint).to.not.be.lessThan(200); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'index2'); + expect(Date.now() - checkpoint).not.toBeLessThan(200); checkpoint = Date.now(); await delay(200); return { hits: { total: 0, hits: [] } }; @@ -118,7 +117,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { createConcatStream([]), ]); - expect(docRecords).to.eql([ + expect(docRecords).toEqual([ { type: 'doc', value: { @@ -139,7 +138,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, ]); sinon.assert.calledTwice(stats.archivedDoc as any); - expect(progress.getTotal()).to.be(2); - expect(progress.getComplete()).to.be(2); + expect(progress.getTotal()).toBe(2); + expect(progress.getComplete()).toBe(2); }); }); diff --git a/packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts similarity index 77% rename from packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts rename to packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index 5ce1a0d434ae6..c30efaf679d5d 100644 --- a/packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -17,13 +17,12 @@ * under the License. */ -import expect from '@kbn/expect'; import { delay } from 'bluebird'; import { createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { Progress } from '../../progress'; -import { createIndexDocRecordsStream } from '../index_doc_records_stream'; -import { createStubStats, createStubClient, createPersonDocRecords } from './stubs'; +import { Progress } from '../progress'; +import { createIndexDocRecordsStream } from './index_doc_records_stream'; +import { createStubStats, createStubClient, createPersonDocRecords } from './__mocks__/stubs'; const recordsToBulkBody = (records: any[]) => { return records.reduce((acc, record) => { @@ -38,8 +37,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { const records = createPersonDocRecords(1); const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records), requestTimeout: 120000, }); @@ -55,24 +54,24 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(1); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(1); + expect(progress.getTotal()).toBe(undefined); }); it('consumes multiple doc records and sends to `_bulk` api together', async () => { const records = createPersonDocRecords(10); const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(0, 1)), requestTimeout: 120000, }); return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(1)), requestTimeout: 120000, }); @@ -88,8 +87,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(10); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(10); + expect(progress.getTotal()).toBe(undefined); }); it('waits until request is complete before sending more', async () => { @@ -99,8 +98,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { const delayMs = 1234; const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(0, 1)), requestTimeout: 120000, }); @@ -108,12 +107,12 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(1)), requestTimeout: 120000, }); - expect(Date.now() - start).to.not.be.lessThan(delayMs); + expect(Date.now() - start).not.toBeLessThan(delayMs); return { ok: true }; }, ]); @@ -125,8 +124,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(10); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(10); + expect(progress.getTotal()).toBe(undefined); }); it('sends a maximum of 300 documents at a time', async () => { @@ -134,18 +133,18 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { const stats = createStubStats(); const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params.body.length).to.eql(1 * 2); + expect(name).toBe('bulk'); + expect(params.body.length).toEqual(1 * 2); return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params.body.length).to.eql(299 * 2); + expect(name).toBe('bulk'); + expect(params.body.length).toEqual(299 * 2); return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params.body.length).to.eql(1 * 2); + expect(name).toBe('bulk'); + expect(params.body.length).toEqual(1 * 2); return { ok: true }; }, ]); @@ -157,8 +156,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(301); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(301); + expect(progress.getTotal()).toBe(undefined); }); it('emits an error if any request fails', async () => { @@ -177,11 +176,11 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); throw new Error('expected stream to emit error'); } catch (err) { - expect(err.message).to.match(/"forcedError":\s*true/); + expect(err.message).toMatch(/"forcedError":\s*true/); } client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(1); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(1); + expect(progress.getTotal()).toBe(undefined); }); }); diff --git a/packages/kbn-es-archiver/src/lib/index.ts b/packages/kbn-es-archiver/src/lib/index.ts index 960d51e411859..ac7569ba735ac 100644 --- a/packages/kbn-es-archiver/src/lib/index.ts +++ b/packages/kbn-es-archiver/src/lib/index.ts @@ -25,6 +25,7 @@ export { createGenerateIndexRecordsStream, deleteKibanaIndices, migrateKibanaIndex, + cleanKibanaIndices, createDefaultSpace, } from './indices'; diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/stubs.ts b/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts similarity index 100% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/stubs.ts rename to packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts similarity index 92% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts rename to packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts index b1a83046f40d6..db3de3378eee1 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts @@ -17,12 +17,11 @@ * under the License. */ -import expect from '@kbn/expect'; import sinon from 'sinon'; import Chance from 'chance'; import { createPromiseFromStreams, createConcatStream, createListStream } from '@kbn/utils'; -import { createCreateIndexStream } from '../create_index_stream'; +import { createCreateIndexStream } from './create_index_stream'; import { createStubStats, @@ -30,7 +29,7 @@ import { createStubDocRecord, createStubClient, createStubLogger, -} from './stubs'; +} from './__mocks__/stubs'; const chance = new Chance(); @@ -49,7 +48,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, log }), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ deletedIndex: 1, createdIndex: 2, }); @@ -68,13 +67,13 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, log }), ]); - expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).to.be.ok(); - expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).to.eql({ + expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).toBe(true); + expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).toEqual({ name: 'existing-index', ignore: [404], }); - expect((client.indices.delete as sinon.SinonSpy).calledOnce).to.be.ok(); - expect((client.indices.delete as sinon.SinonSpy).args[0][0]).to.eql({ + expect((client.indices.delete as sinon.SinonSpy).calledOnce).toBe(true); + expect((client.indices.delete as sinon.SinonSpy).args[0][0]).toEqual({ index: ['actual-index'], }); sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing @@ -93,7 +92,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(output).to.eql([createStubDocRecord('index', 1), createStubDocRecord('index', 2)]); + expect(output).toEqual([createStubDocRecord('index', 1), createStubDocRecord('index', 2)]); }); it('creates aliases', async () => { @@ -133,7 +132,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(output).to.eql(randoms); + expect(output).toEqual(randoms); }); it('passes through non-record values', async () => { @@ -147,7 +146,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(output).to.eql(nonRecordValues); + expect(output).toEqual(nonRecordValues); }); }); @@ -169,13 +168,13 @@ describe('esArchiver: createCreateIndexStream()', () => { }), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ skippedIndex: 1, createdIndex: 1, }); sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing - expect((client.indices.create as sinon.SinonSpy).args[0][0]).to.have.property( + expect((client.indices.create as sinon.SinonSpy).args[0][0]).toHaveProperty( 'index', 'new-index' ); @@ -203,15 +202,15 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ skippedIndex: 1, createdIndex: 1, }); sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing - expect(output).to.have.length(2); - expect(output).to.eql([ + expect(output).toHaveLength(2); + expect(output).toEqual([ createStubDocRecord('new-index', 1), createStubDocRecord('new-index', 2), ]); diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts similarity index 96% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts rename to packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts index 3c9d866700005..ec588d5e7dae2 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts @@ -21,14 +21,14 @@ import sinon from 'sinon'; import { createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { createDeleteIndexStream } from '../delete_index_stream'; +import { createDeleteIndexStream } from './delete_index_stream'; import { createStubStats, createStubClient, createStubIndexRecord, createStubLogger, -} from './stubs'; +} from './__mocks__/stubs'; const log = createStubLogger(); diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts similarity index 76% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts rename to packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts index d2c9f1274e60f..fc5e86217038f 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts @@ -18,12 +18,11 @@ */ import sinon from 'sinon'; -import expect from '@kbn/expect'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createStubClient, createStubStats } from './stubs'; +import { createStubClient, createStubStats } from './__mocks__/stubs'; -import { createGenerateIndexRecordsStream } from '../generate_index_records_stream'; +import { createGenerateIndexRecordsStream } from './generate_index_records_stream'; describe('esArchiver: createGenerateIndexRecordsStream()', () => { it('consumes index names and queries for the mapping of each', async () => { @@ -36,7 +35,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createGenerateIndexRecordsStream(client, stats), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ archivedIndex: 4, }); @@ -56,12 +55,12 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { ]); const params = (client.indices.get as sinon.SinonSpy).args[0][0]; - expect(params).to.have.property('filterPath'); + expect(params).toHaveProperty('filterPath'); const filters: string[] = params.filterPath; - expect(filters.some((path) => path.includes('index.creation_date'))).to.be(true); - expect(filters.some((path) => path.includes('index.uuid'))).to.be(true); - expect(filters.some((path) => path.includes('index.version'))).to.be(true); - expect(filters.some((path) => path.includes('index.provided_name'))).to.be(true); + expect(filters.some((path) => path.includes('index.creation_date'))).toBe(true); + expect(filters.some((path) => path.includes('index.uuid'))).toBe(true); + expect(filters.some((path) => path.includes('index.version'))).toBe(true); + expect(filters.some((path) => path.includes('index.provided_name'))).toBe(true); }); it('produces one index record for each index name it receives', async () => { @@ -74,19 +73,19 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createConcatStream([]), ]); - expect(indexRecords).to.have.length(3); + expect(indexRecords).toHaveLength(3); - expect(indexRecords[0]).to.have.property('type', 'index'); - expect(indexRecords[0]).to.have.property('value'); - expect(indexRecords[0].value).to.have.property('index', 'index1'); + expect(indexRecords[0]).toHaveProperty('type', 'index'); + expect(indexRecords[0]).toHaveProperty('value'); + expect(indexRecords[0].value).toHaveProperty('index', 'index1'); - expect(indexRecords[1]).to.have.property('type', 'index'); - expect(indexRecords[1]).to.have.property('value'); - expect(indexRecords[1].value).to.have.property('index', 'index2'); + expect(indexRecords[1]).toHaveProperty('type', 'index'); + expect(indexRecords[1]).toHaveProperty('value'); + expect(indexRecords[1].value).toHaveProperty('index', 'index2'); - expect(indexRecords[2]).to.have.property('type', 'index'); - expect(indexRecords[2]).to.have.property('value'); - expect(indexRecords[2].value).to.have.property('index', 'index3'); + expect(indexRecords[2]).toHaveProperty('type', 'index'); + expect(indexRecords[2]).toHaveProperty('value'); + expect(indexRecords[2].value).toHaveProperty('index', 'index3'); }); it('understands aliases', async () => { @@ -99,7 +98,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createConcatStream([]), ]); - expect(indexRecords).to.eql([ + expect(indexRecords).toEqual([ { type: 'index', value: { diff --git a/packages/kbn-es-archiver/src/lib/indices/index.ts b/packages/kbn-es-archiver/src/lib/indices/index.ts index 289ac87feb9a5..076582ddde8ab 100644 --- a/packages/kbn-es-archiver/src/lib/indices/index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/index.ts @@ -20,4 +20,9 @@ export { createCreateIndexStream } from './create_index_stream'; export { createDeleteIndexStream } from './delete_index_stream'; export { createGenerateIndexRecordsStream } from './generate_index_records_stream'; -export { migrateKibanaIndex, deleteKibanaIndices, createDefaultSpace } from './kibana_index'; +export { + migrateKibanaIndex, + deleteKibanaIndices, + cleanKibanaIndices, + createDefaultSpace, +} from './kibana_index'; diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 3599911735b8d..50fabad1fa26f 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -73,6 +73,7 @@ export async function migrateKibanaIndex({ body: { dynamic: true, }, + ignore: [404], } as any); await kbnClient.savedObjects.migrate(); diff --git a/packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts similarity index 89% rename from packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts rename to packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts index cf67ee2071c10..8fba5668e972d 100644 --- a/packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts @@ -18,11 +18,10 @@ */ import Chance from 'chance'; -import expect from '@kbn/expect'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createFilterRecordsStream } from '../filter_records_stream'; +import { createFilterRecordsStream } from './filter_records_stream'; const chance = new Chance(); @@ -42,7 +41,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { createConcatStream([]), ]); - expect(output).to.eql([]); + expect(output).toEqual([]); }); it('produces record values that have a matching type', async () => { @@ -61,7 +60,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { createConcatStream([]), ]); - expect(output).to.have.length(3); - expect(output.map((o) => o.type)).to.eql([type1, type1, type1]); + expect(output).toHaveLength(3); + expect(output.map((o) => o.type)).toEqual([type1, type1, type1]); }); }); diff --git a/packages/kbn-i18n/GUIDELINE.md b/packages/kbn-i18n/GUIDELINE.md index b7c1371d59ea4..437e73bb27019 100644 --- a/packages/kbn-i18n/GUIDELINE.md +++ b/packages/kbn-i18n/GUIDELINE.md @@ -387,6 +387,50 @@ Splitting sentences into several keys often inadvertently presumes a grammar, a ### Unit tests +#### How to test `FormattedMessage` and `i18n.translate()` components. + +To make `FormattedMessage` component work properly, wrapping it with `I18nProvider` is required. In development/production app, this is done in the ancestor components and developers don't have to worry about that. + +But when unit-testing them, no other component provides that wrapping. That's why `shallowWithI18nProvider` and `mountWithI18nProvider` helpers are created. + +For example, there is a component that has `FormattedMessage` inside, like `SaveModal` component: + +```js +// ... +export const SaveModal = (props) => { + return ( +
+ {/* Other things. */} + + + + {/* More other things. */} +
+ ) +} +``` + +To test `SaveModal` component, it should be wrapped with `I18nProvider` by using `shallowWithI18nProvider`: + +```js +// ... +it('should render normally', async () => { + const component = shallowWithI18nProvider( + + ); + + expect(component).toMatchSnapshot(); +}); +// ... +``` + +If a component uses only `i18n.translate()`, it doesn't need `I18nProvider`. In that case, you can test them with `shallow` and `mount` functions that `enzyme` providers out of the box. + +#### How to test `injectI18n` HOC components. + Testing React component that uses the `injectI18n` higher-order component is more complicated because `injectI18n()` creates a wrapper component around the original component. With shallow rendering only top level component is rendered, that is a wrapper itself, not the original component. Since we want to test the rendering of the original component, we need to access it via the wrapper's `WrappedComponent` property. Its value will be the component we passed into `injectI18n()`. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 08d883a7cbb4d..67287089489e1 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,6 +102,7 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 - runtimeFields: 41752 + runtimeFields: 10000 stackAlerts: 29684 presentationUtil: 28545 + runtimeFieldEditor: 46986 diff --git a/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap b/packages/kbn-std/src/__snapshots__/ensure_no_unsafe_properties.test.ts.snap similarity index 100% rename from src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap rename to packages/kbn-std/src/__snapshots__/ensure_no_unsafe_properties.test.ts.snap diff --git a/src/core/server/http/prototype_pollution/validate_object.test.ts b/packages/kbn-std/src/ensure_no_unsafe_properties.test.ts similarity index 89% rename from src/core/server/http/prototype_pollution/validate_object.test.ts rename to packages/kbn-std/src/ensure_no_unsafe_properties.test.ts index 23d6c4ae3b49f..c12626b8d777e 100644 --- a/src/core/server/http/prototype_pollution/validate_object.test.ts +++ b/packages/kbn-std/src/ensure_no_unsafe_properties.test.ts @@ -17,14 +17,14 @@ * under the License. */ -import { validateObject } from './validate_object'; +import { ensureNoUnsafeProperties } from './ensure_no_unsafe_properties'; test(`fails on circular references`, () => { const foo: Record = {}; foo.myself = foo; expect(() => - validateObject({ + ensureNoUnsafeProperties({ payload: foo, }) ).toThrowErrorMatchingInlineSnapshot(`"circular reference detected"`); @@ -57,7 +57,7 @@ test(`fails on circular references`, () => { [property]: value, }; test(`can submit ${JSON.stringify(obj)}`, () => { - expect(() => validateObject(obj)).not.toThrowError(); + expect(() => ensureNoUnsafeProperties(obj)).not.toThrowError(); }); }); }); @@ -74,6 +74,6 @@ test(`fails on circular references`, () => { JSON.parse(`{ "foo": { "bar": { "constructor": { "prototype" : null } } } }`), ].forEach((value) => { test(`can't submit ${JSON.stringify(value)}`, () => { - expect(() => validateObject(value)).toThrowErrorMatchingSnapshot(); + expect(() => ensureNoUnsafeProperties(value)).toThrowErrorMatchingSnapshot(); }); }); diff --git a/src/core/server/http/prototype_pollution/validate_object.ts b/packages/kbn-std/src/ensure_no_unsafe_properties.ts similarity index 97% rename from src/core/server/http/prototype_pollution/validate_object.ts rename to packages/kbn-std/src/ensure_no_unsafe_properties.ts index cab6ce295ce92..47cbea5ecf3ee 100644 --- a/src/core/server/http/prototype_pollution/validate_object.ts +++ b/packages/kbn-std/src/ensure_no_unsafe_properties.ts @@ -31,7 +31,7 @@ const hasOwnProperty = (obj: any, property: string) => const isObject = (obj: any) => typeof obj === 'object' && obj !== null; // we're using a stack instead of recursion so we aren't limited by the call stack -export function validateObject(obj: any) { +export function ensureNoUnsafeProperties(obj: any) { if (!isObject(obj)) { return; } diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index c111428017539..a5b5088f9105f 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -27,4 +27,5 @@ export { withTimeout } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; +export { ensureNoUnsafeProperties } from './ensure_no_unsafe_properties'; export * from './rxjs_7'; diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 8f53d6f7cf58b..2dfc9ded66201 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -141,22 +141,27 @@ export function runFtrCli() { config: 'test/functional/config.js', }, help: ` - --config=path path to a config file - --bail stop tests after the first failure - --grep pattern used to select which tests to run - --invert invert grep to exclude tests - --include=file a test file to be included, pass multiple times for multiple files - --exclude=file a test file to be excluded, pass multiple times for multiple files - --include-tag=tag a tag to be included, pass multiple times for multiple tags - --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags - --test-stats print the number of tests (included and excluded) to STDERR - --updateBaselines replace baseline screenshots with whatever is generated from the test - --updateSnapshots replace inline and file snapshots with whatever is generated from the test - -u replace both baseline screenshots and snapshots - --kibana-install-dir directory where the Kibana install being tested resides - --throttle enable network throttling in Chrome browser - --headless run browser in headless mode - `, + --config=path path to a config file + --bail stop tests after the first failure + --grep pattern used to select which tests to run + --invert invert grep to exclude tests + --include=file a test file to be included, pass multiple times for multiple files + --exclude=file a test file to be excluded, pass multiple times for multiple files + --include-tag=tag a tag to be included, pass multiple times for multiple tags. Only + suites which have one of the passed include-tag tags will be executed. + When combined with the --exclude-tag flag both conditions must be met + for a suite to run. + --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags. Any suite + which has any of the exclude-tags will be excluded. When combined with + the --include-tag flag both conditions must be met for a suite to run. + --test-stats print the number of tests (included and excluded) to STDERR + --updateBaselines replace baseline screenshots with whatever is generated from the test + --updateSnapshots replace inline and file snapshots with whatever is generated from the test + -u replace both baseline screenshots and snapshots + --kibana-install-dir directory where the Kibana install being tested resides + --throttle enable network throttling in Chrome browser + --headless run browser in headless mode + `, }, } ); diff --git a/rfcs/images/background_sessions_client.png b/rfcs/images/search_sessions_client.png similarity index 100% rename from rfcs/images/background_sessions_client.png rename to rfcs/images/search_sessions_client.png diff --git a/rfcs/images/background_sessions_server.png b/rfcs/images/search_sessions_server.png similarity index 100% rename from rfcs/images/background_sessions_server.png rename to rfcs/images/search_sessions_server.png diff --git a/rfcs/text/0013_background_sessions.md b/rfcs/text/0013_search_sessions.md similarity index 81% rename from rfcs/text/0013_background_sessions.md rename to rfcs/text/0013_search_sessions.md index 056149e770448..659f1933a86f9 100644 --- a/rfcs/text/0013_background_sessions.md +++ b/rfcs/text/0013_search_sessions.md @@ -5,19 +5,19 @@ - Architecture diagram: https://app.lucidchart.com/documents/edit/cf35b512-616a-4734-bc72-43dde70dbd44/0_0 - Mockups: https://www.figma.com/proto/FD2M7MUpLScJKOyYjfbmev/ES-%2F-Query-Management-v4?node-id=440%3A1&viewport=984%2C-99%2C0.09413627535104752&scaling=scale-down - Old issue: https://github.com/elastic/kibana/issues/53335 -- Background search roadmap: https://github.com/elastic/kibana/issues/61738 +- Search Sessions roadmap: https://github.com/elastic/kibana/issues/61738 - POC: https://github.com/elastic/kibana/pull/64641 # Summary -Background Sessions will enable Kibana applications and solutions to start a group of related search requests (such as those coming from a single load of a dashboard or SIEM timeline), navigate away or close the browser, then retrieve the results when they have completed. +Search Sessions will enable Kibana applications and solutions to start a group of related search requests (such as those coming from a single load of a dashboard or SIEM timeline), navigate away or close the browser, then retrieve the results when they have completed. # Basic example -At its core, background sessions are enabled via several new APIs, that: +At its core, search sessions are enabled via several new APIs, that: - Start a session, associating multiple search requests with a single entity - Store the session (and continue search requests in the background) -- Restore the background session +- Restore the saved search session ```ts const searchService = dataPluginStart.search; @@ -26,7 +26,7 @@ if (appState.sessionId) { // If we are restoring a session, set the session ID in the search service searchService.session.restore(sessionId); } else { - // Otherwise, start a new background session to associate our search requests + // Otherwise, start a new search session to associate our search requests appState.sessionId = searchService.session.start(); } @@ -41,7 +41,7 @@ const response$ = await searchService.search(request); // Calling `session.store()`, creates a saved object for this session, allowing the user to navigate away. // The session object will be saved with all async search IDs that were executed so far. // Any follow up searches executed with this sessionId will be saved into this object as well. -const backgroundSession = await searchService.session.store(); +const searchSession = await searchService.session.store(); ``` # Motivation @@ -73,20 +73,20 @@ We call this entity a `session`, and when a user decides that they want to conti This diagram matches any case where `data.search` is called from the front end: -![image](../images/background_sessions_client.png) +![image](../images/search_sessions_client.png) ### Server side search This case happens if the server is the one to invoke the `data.search` endpoint, for example with TSVB. -![image](../images/background_sessions_server.png) +![image](../images/search_sessions_server.png) ## Data and Saved Objects -### Background Session Status +### Search Session Status ```ts -export enum BackgroundSessionStatus { +export enum SearchSessionStatus { Running, // The session has at least one running search ID associated with it. Done, // All search IDs associated with this session have completed. Error, // At least one search ID associated with this session had an error. @@ -96,27 +96,27 @@ export enum BackgroundSessionStatus { ### Saved Object Structure -The saved object created for a background session will be scoped to a single space, and will be a `hidden` saved object +The saved object created for a search session will be scoped to a single space, and will be a `hidden` saved object (so that it doesn't show in the management listings). We will provide a separate interface for users to manage their own -background sessions (which will use the `list`, `expire`, and `extend` methods described below, which will be restricted +saved search sessions (which will use the `list`, `expire`, and `extend` methods described below, which will be restricted per-user). ```ts -interface BackgroundSessionAttributes extends SavedObjectAttributes { +interface SearchSessionAttributes extends SavedObjectAttributes { sessionId: string; userId: string; // Something unique to the user who generated this session, like username/realm-name/realm-type - status: BackgroundSessionStatus; + status: SearchSessionStatus; name: string; creation: Date; expiration: Date; idMapping: { [key: string]: string }; - url: string; // A URL relative to the Kibana root to retrieve the results of a completed background session (and/or to return to an incomplete view) - metadata: { [key: string]: any } // Any data the specific application requires to restore a background session view + url: string; // A URL relative to the Kibana root to retrieve the results of a completed search session (and/or to return to an incomplete view) + metadata: { [key: string]: any } // Any data the specific application requires to restore a search session view } ``` -The URL that is provided will need to be generated by the specific application implementing background sessions. We -recommend using the URL generator to ensure that URLs are backwards-compatible since background sessions may exist as +The URL that is provided will need to be generated by the specific application implementing search sessions. We +recommend using the URL generator to ensure that URLs are backwards-compatible since search sessions may exist as long as a user continues to extend the expiration. ## Frontend Services @@ -153,10 +153,10 @@ interface ISessionService { * @param sessionId Session ID to store. Probably retrieved from `sessionService.get()`. * @param name A display name for the session. * @param url TODO: is the URL provided here? How? - * @returns The stored `BackgroundSessionAttributes` object + * @returns The stored `SearchSessionAttributes` object * @throws Throws an error in OSS. */ - store: (sessionId: string, name: string, url: string) => Promise + store: (sessionId: string, name: string, url: string) => Promise /** * @returns Is the current session stored (i.e. is there a saved object corresponding with this sessionId). @@ -188,17 +188,17 @@ interface ISessionService { /** * @param sessionId the ID of the session to retrieve the saved object. - * @returns a filtered list of BackgroundSessionAttributes objects. + * @returns a filtered list of SearchSessionAttributes objects. * @throws Throws an error in OSS. */ - get: (sessionId: string) => Promise + get: (sessionId: string) => Promise /** - * @param options The options to query for specific background session saved objects. - * @returns a filtered list of BackgroundSessionAttributes objects. + * @param options The options to query for specific search session saved objects. + * @returns a filtered list of SearchSessionAttributes objects. * @throws Throws an error in OSS. */ - list: (options: SavedObjectsFindOptions) => Promise + list: (options: SavedObjectsFindOptions) => Promise /** * Clears out any session info as well as the current session. Called internally whenever the user navigates @@ -241,12 +241,12 @@ attempt to find the correct id within the saved object, and use it to retrieve t ```ts interface ISessionService { /** - * Adds a search ID to a Background Session, if it exists. + * Adds a search ID to a Search Session, if it exists. * Also extends the expiration of the search ID to match the session's expiration. * @param request * @param sessionId * @param searchId - * @returns true if id was added, false if Background Session doesn't exist or if there was an error while updating. + * @returns true if id was added, false if Search Session doesn't exist or if there was an error while updating. * @throws an error if `searchId` already exists in the mapping for this `sessionId` */ trackSearchId: ( @@ -256,21 +256,21 @@ interface ISessionService { ) => Promise /** - * Get a Background Session object. + * Get a Search Session object. * @param request * @param sessionId - * @returns the Background Session object if exists, or undefined. + * @returns the Search Session object if exists, or undefined. */ get: async ( request: KibanaRequest, sessionId: string - ) => Promise + ) => Promise /** - * Get a searchId from a Background Session object. + * Get a searchId from a Search Session object. * @param request * @param sessionId - * @returns the searchID if exists on the Background Session, or undefined. + * @returns the searchID if exists on the Search Session, or undefined. */ getSearchId: async ( request: KibanaRequest, @@ -283,7 +283,7 @@ interface ISessionService { * @param sessionId Session ID to store. Probably retrieved from `sessionService.get()`. * @param searchIdMap A mapping of hashed requests mapped to the corresponding searchId. * @param url TODO: is the URL provided here? How? - * @returns The stored `BackgroundSessionAttributes` object + * @returns The stored `SearchSessionAttributes` object * @throws Throws an error in OSS. * @internal (Consumers should use searchInterceptor.sendToBackground()) */ @@ -293,7 +293,7 @@ interface ISessionService { name: string, url: string, searchIdMapping?: Record - ) => Promise + ) => Promise /** * Mark a session as and all associated searchIds as expired. @@ -322,7 +322,7 @@ interface ISessionService { ) => Promise /** - * Get a list of background session objects. + * Get a list of Search Session objects. * @param request * @param sessionId * @returns success status @@ -330,7 +330,7 @@ interface ISessionService { */ list: async ( request: KibanaRequest, - ) => Promise + ) => Promise /** * Update the status of a given session @@ -343,7 +343,7 @@ interface ISessionService { updateStatus: async ( request: KibanaRequest, sessionId: string, - status: BackgroundSessionStatus + status: SearchSessionStatus ) => Promise } @@ -381,13 +381,13 @@ Each route exposes the corresponding method from the Session Service (used only ### Search Strategy Integration -If the `EnhancedEsSearchStrategy` receives a `restore` option, it will attempt reloading data using the Background Session saved object matching the provided `sessionId`. If there are any errors during that process, the strategy will return an error response and *not attempt to re-run the request. +If the `EnhancedEsSearchStrategy` receives a `restore` option, it will attempt reloading data using the Search Session saved object matching the provided `sessionId`. If there are any errors during that process, the strategy will return an error response and *not attempt to re-run the request. The strategy will track the asyncId on the server side, if `trackId` option is provided. ### Monitoring Service -The `data` plugin will register a task with the task manager, periodically monitoring the status of incomplete background sessions. +The `data` plugin will register a task with the task manager, periodically monitoring the status of incomplete search sessions. It will query the list of all incomplete sessions, and check the status of each search that is executing. If the search requests are all complete, it will update the corresponding saved object to have a `status` of `complete`. If any of the searches return an error, it will update the saved object to an `error` state. If the search requests have expired, it will update the saved object to an `expired` state. Expired sessions will be purged once they are older than the time definedby the `EXPIRED_SESSION_TTL` advanced setting. @@ -405,23 +405,23 @@ There are two potential scenarios: Both scenarios require careful attention during the UI design and implementation. -The former can be resolved by clearly displaying the creation time of the restored Background Session. We could also attempt translating relative dates to absolute one's, but this might be challenging as relative dates may appear deeply nested within the DSL. +The former can be resolved by clearly displaying the creation time of the restored Search Session. We could also attempt translating relative dates to absolute one's, but this might be challenging as relative dates may appear deeply nested within the DSL. The latter case happens at the moment for the timepicker only: The relative date is being translated each time into an absolute one, before being sent to Elasticsearch. In order to avoid issues, we'll have to make sure that restore URLs are generated with an absolute date, to make sure they are restored correctly. #### Changing a restored session -If you have restored a Background Session, making any type of change to it (time range, filters, etc.) will trigger new (potentially long) searches. There should be a clear indication in the UI that the data is no longer stored. A user then may choose to send it to background, resulting in a new Background Session being saved. +If you have restored a Search Session, making any type of change to it (time range, filters, etc.) will trigger new (potentially long) searches. There should be a clear indication in the UI that the data is no longer stored. A user then may choose to send it to background, resulting in a new Search Session being saved. #### Loading an errored \ expired \ canceled session -When trying to restore a Background Session, if any of the requests hashes don't match the ones saved, or if any of the saved async search IDs are expired, a meaningful error code will be returned by the server **by those requests**. It is each application's responsibility to handle these errors appropriately. +When trying to restore a Search Session, if any of the requests hashes don't match the ones saved, or if any of the saved async search IDs are expired, a meaningful error code will be returned by the server **by those requests**. It is each application's responsibility to handle these errors appropriately. In such a scenario, the session will be partially restored. #### Extending Expiration -Sessions are given an expiration date defined in an advanced setting (5 days by default). This expiration date is measured from the time the Background Session is saved, and it includes the time it takes to generate the results. +Sessions are given an expiration date defined in an advanced setting (5 days by default). This expiration date is measured from the time the Search Session is saved, and it includes the time it takes to generate the results. A session's expiration date may be extended indefinitely. However, if a session was canceled or has already expired, it needs to be re-run. @@ -444,7 +444,7 @@ so we feel comfortable moving forward with this approach. Two potential drawbacks stem from storing things in server memory. If a Kibana server is restarted, in-memory results will be lost. (This can be an issue if a search request has started, and the user has sent to background, but the -background session saved object has not yet been updated with the search request ID.) In such cases, the user interface +search session saved object has not yet been updated with the search request ID.) In such cases, the user interface will need to indicate errors for requests that were not stored in the saved object. There is also the consideration of the memory footprint of the Kibana server; however, since @@ -452,7 +452,7 @@ we are only storing a hash of the request and search request ID, and are periodi Services and Routes), we do not anticipate the footprint to increase significantly. The results of search requests that have been sent to the background will be stored in Elasticsearch for several days, -even if they will only be retrieved once. This will be mitigated by allowing the user manually delete a background +even if they will only be retrieved once. This will be mitigated by allowing the user manually delete a search session object after it has been accessed. # Alternatives @@ -463,7 +463,7 @@ What other designs have been considered? What is the impact of not doing this? (See "Basic example" above.) -Any application or solution that uses the `data` plugin `search` services will be able to facilitate background sessions +Any application or solution that uses the `data` plugin `search` services will be able to facilitate search sessions fairly simply. The public side will need to create/clear sessions when appropriate, and ensure the `sessionId` is sent with all search requests. It will also need to ensure that any necessary application data, as well as a `restoreUrl` is sent when creating the saved object. diff --git a/scripts/ensure_all_tests_in_ci_group.js b/scripts/ensure_all_tests_in_ci_group.js new file mode 100644 index 0000000000000..d189aac8f62e8 --- /dev/null +++ b/scripts/ensure_all_tests_in_ci_group.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('../src/dev/run_ensure_all_tests_in_ci_group'); diff --git a/src/core/public/application/capabilities/capabilities_service.test.ts b/src/core/public/application/capabilities/capabilities_service.test.ts index 286a93fdc2398..aa9c10ecfb2b2 100644 --- a/src/core/public/application/capabilities/capabilities_service.test.ts +++ b/src/core/public/application/capabilities/capabilities_service.test.ts @@ -41,11 +41,36 @@ describe('#start', () => { http.post.mockReturnValue(Promise.resolve(mockedCapabilities)); }); + it('requests default capabilities on anonymous paths', async () => { + http.anonymousPaths.isAnonymous.mockReturnValue(true); + const service = new CapabilitiesService(); + const appIds = ['app1', 'app2', 'legacyApp1', 'legacyApp2']; + const { capabilities } = await service.start({ + http, + appIds, + }); + + expect(http.post).toHaveBeenCalledWith('/api/core/capabilities', { + query: { + useDefaultCapabilities: true, + }, + body: JSON.stringify({ applications: appIds }), + }); + + // @ts-expect-error TypeScript knows this shouldn't be possible + expect(() => (capabilities.foo = 'foo')).toThrowError(); + }); + it('only returns capabilities for given appIds', async () => { const service = new CapabilitiesService(); + const appIds = ['app1', 'app2', 'legacyApp1', 'legacyApp2']; const { capabilities } = await service.start({ http, - appIds: ['app1', 'app2', 'legacyApp1', 'legacyApp2'], + appIds, + }); + + expect(http.post).toHaveBeenCalledWith('/api/core/capabilities', { + body: JSON.stringify({ applications: appIds }), }); // @ts-expect-error TypeScript knows this shouldn't be possible diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index 1164164aec4c5..156b75b2d8abe 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -38,7 +38,9 @@ export interface CapabilitiesStart { */ export class CapabilitiesService { public async start({ appIds, http }: StartDeps): Promise { + const useDefaultCapabilities = http.anonymousPaths.isAnonymous(window.location.pathname); const capabilities = await http.post('/api/core/capabilities', { + query: useDefaultCapabilities ? { useDefaultCapabilities } : undefined, body: JSON.stringify({ applications: appIds }), }); diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index 44cd864278325..b11e7e47f4ae7 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,5 +1,19 @@ @include euiHeaderAffordForFixed; +.euiDataGrid__restrictBody { + .headerGlobalNav, + .kbnQueryBar { + display: none; + } +} + +.euiDataGrid__restrictBody.euiBody--headerIsFixed { + .euiFlyout { + top: 0; + height: 100%; + } +} + .chrHeaderHelpMenu__version { text-transform: none; } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index b8843b5c85595..12266ec8de2e4 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -43,6 +43,9 @@ export class DocLinksService { urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#url-template-variables`, }, + discover: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/discover.html`, + }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation-configuration.html`, @@ -72,6 +75,7 @@ export class DocLinksService { aggs: { date_histogram: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-datehistogram-aggregation.html`, date_range: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-daterange-aggregation.html`, + date_format_pattern: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-daterange-aggregation.html#date-format-pattern`, filter: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-filter-aggregation.html`, filters: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-filters-aggregation.html`, geohash_grid: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-geohashgrid-aggregation.html`, @@ -101,12 +105,14 @@ export class DocLinksService { sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`, top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`, }, + runtimeFields: `${ELASTICSEARCH_DOCS}runtime.html`, scriptedFields: { scriptFields: `${ELASTICSEARCH_DOCS}search-request-script-fields.html`, scriptAggs: `${ELASTICSEARCH_DOCS}search-aggregations.html#_values_source`, painless: `${ELASTICSEARCH_DOCS}modules-scripting-painless.html`, painlessApi: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-api-reference.html`, painlessSyntax: `${ELASTICSEARCH_DOCS}modules-scripting-painless-syntax.html`, + painlessLanguage: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-lang-spec.html`, luceneExpressions: `${ELASTICSEARCH_DOCS}modules-scripting-expression.html`, }, indexPatterns: { @@ -115,6 +121,13 @@ export class DocLinksService { }, addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, + elasticsearch: { + remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, + remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, + remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, + scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, + transportSettings: `${ELASTICSEARCH_DOCS}modules-transport.html`, + }, siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, @@ -147,16 +160,76 @@ export class DocLinksService { featureImportance: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-feature-importance.html`, outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`, + classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-class-aucroc`, }, transforms: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/transforms.html`, + guide: `${ELASTICSEARCH_DOCS}transforms.html`, }, visualize: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`, + guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#timelion-deprecation`, lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`, + lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#create-panels-with-lens`, maps: `${ELASTIC_WEBSITE_URL}maps`, }, + observability: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, + }, + alerting: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/managing-alerts-and-actions.html`, + actionTypes: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/action-types.html`, + emailAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, + generalSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`, + indexAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html`, + indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-types.html#alert-type-index-threshold`, + pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`, + preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-action-types-and-connectors.html`, + serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html`, + setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`, + slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html`, + teamsAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/teams-action-type.html`, + }, + maps: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-maps.html`, + }, + monitoring: { + alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, + monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, + monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, + }, + security: { + apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`, + clusterPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-cluster`, + elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, + indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, + kibanaTLS: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`, + kibanaPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-privileges.html`, + mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, + }, + watcher: { + jiraAction: `${ELASTICSEARCH_DOCS}actions-jira.html`, + pagerDutyAction: `${ELASTICSEARCH_DOCS}actions-pagerduty.html`, + slackAction: `${ELASTICSEARCH_DOCS}actions-slack.html`, + ui: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/watcher-ui.html`, + }, + ccs: { + guide: `${ELASTICSEARCH_DOCS}modules-cross-cluster-search.html`, + }, + apis: { + createIndex: `${ELASTICSEARCH_DOCS}indices-create-index.html`, + createSnapshotLifecylePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, + createRoleMapping: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html`, + createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, + createPipeline: `${ELASTICSEARCH_DOCS}put-pipeline-api.html`, + createTransformRequest: `${ELASTICSEARCH_DOCS}put-transform.html#put-transform-request-body`, + executeWatchActionModes: `${ELASTICSEARCH_DOCS}watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`, + openIndex: `${ELASTICSEARCH_DOCS}indices-open-close.html`, + putComponentTemplate: `${ELASTICSEARCH_DOCS}indices-component-template.html`, + painlessExecute: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html`, + putComponentTemplateMetadata: `${ELASTICSEARCH_DOCS}indices-component-template.html#component-templates-metadata`, + putWatch: `${ELASTICSEARCH_DOCS}/watcher-api-put-watch.html`, + updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`, + }, }, }); } @@ -174,6 +247,7 @@ export interface DocLinksStart { readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; + readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; @@ -203,6 +277,7 @@ export interface DocLinksStart { readonly aggs: { readonly date_histogram: string; readonly date_range: string; + readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; @@ -232,6 +307,7 @@ export interface DocLinksStart { readonly sum: string; readonly top_hits: string; }; + readonly runtimeFields: string; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; @@ -246,6 +322,7 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; + readonly elasticsearch: Record; readonly siem: { readonly guide: string; readonly gettingStarted: string; @@ -263,5 +340,13 @@ export interface DocLinksStart { readonly ml: Record; readonly transforms: Record; readonly visualize: Record; + readonly apis: Record; + readonly observability: Record; + readonly alerting: Record; + readonly maps: Record; + readonly monitoring: Record; + readonly security: Record; + readonly watcher: Record; + readonly ccs: Record; }; } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 51375072d3e5a..ea83674ed9d9c 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -148,7 +148,7 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectsNamespaceType, } from './saved_objects'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0303eb62b6419..c5b49519ef7b2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -494,6 +494,7 @@ export interface DocLinksStart { readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; + readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; @@ -523,6 +524,7 @@ export interface DocLinksStart { readonly aggs: { readonly date_histogram: string; readonly date_range: string; + readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; @@ -552,6 +554,7 @@ export interface DocLinksStart { readonly sum: string; readonly top_hits: string; }; + readonly runtimeFields: string; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; @@ -566,6 +569,7 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; + readonly elasticsearch: Record; readonly siem: { readonly guide: string; readonly gettingStarted: string; @@ -583,6 +587,14 @@ export interface DocLinksStart { readonly ml: Record; readonly transforms: Record; readonly visualize: Record; + readonly apis: Record; + readonly observability: Record; + readonly alerting: Record; + readonly maps: Record; + readonly monitoring: Record; + readonly security: Record; + readonly watcher: Record; + readonly ccs: Record; }; } @@ -1234,7 +1246,7 @@ export interface SavedObjectsImportConflictError { } // @public -export interface SavedObjectsImportError { +export interface SavedObjectsImportFailure { // (undocumented) error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; // (undocumented) @@ -1265,7 +1277,7 @@ export interface SavedObjectsImportMissingReferencesError { // @public export interface SavedObjectsImportResponse { // (undocumented) - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; // (undocumented) success: boolean; // (undocumented) diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index cc8fce0884ddf..54427638e9154 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -43,7 +43,7 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectsNamespaceType, } from '../../server/types'; diff --git a/src/core/server/capabilities/capabilities_service.ts b/src/core/server/capabilities/capabilities_service.ts index f0be9743d4d60..9af945d17b2ad 100644 --- a/src/core/server/capabilities/capabilities_service.ts +++ b/src/core/server/capabilities/capabilities_service.ts @@ -76,7 +76,19 @@ export interface CapabilitiesSetup { * ```ts * // my-plugin/server/plugin.ts * public setup(core: CoreSetup, deps: {}) { - * core.capabilities.registerSwitcher((request, capabilities) => { + * core.capabilities.registerSwitcher((request, capabilities, useDefaultCapabilities) => { + * // useDefaultCapabilities is a special case that switchers typically don't have to concern themselves with. + * // The default capabilities are typically the ones you provide in your CapabilitiesProvider, but this flag + * // gives each switcher an opportunity to change the default capabilities of other plugins' capabilities. + * // For example, you may decide to flip another plugin's capability to false if today is Tuesday, + * // but you wouldn't want to do this when we are requesting the default set of capabilities. + * if (useDefaultCapabilities) { + * return { + * somePlugin: { + * featureEnabledByDefault: true + * } + * } + * } * if(myPluginApi.shouldRestrictSomePluginBecauseOf(request)) { * return { * somePlugin: { @@ -150,7 +162,7 @@ export class CapabilitiesService { public start(): CapabilitiesStart { return { - resolveCapabilities: (request) => this.resolveCapabilities(request, []), + resolveCapabilities: (request) => this.resolveCapabilities(request, [], false), }; } } diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts index 17f2c77bbf660..4217dd98ae735 100644 --- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts +++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts @@ -72,17 +72,57 @@ describe('CapabilitiesService', () => { `); }); - it('uses the service capabilities providers', async () => { - serviceSetup.registerProvider(() => ({ + it('uses the service capabilities providers and switchers', async () => { + const getInitialCapabilities = () => ({ catalogue: { something: true, }, - })); + management: {}, + navLinks: {}, + }); + serviceSetup.registerProvider(() => getInitialCapabilities()); + + const switcher = jest.fn((_, capabilities) => capabilities); + serviceSetup.registerSwitcher(switcher); const result = await supertest(httpSetup.server.listener) .post('/api/core/capabilities') .send({ applications: [] }) .expect(200); + + expect(switcher).toHaveBeenCalledTimes(1); + expect(switcher).toHaveBeenCalledWith(expect.anything(), getInitialCapabilities(), false); + expect(result.body).toMatchInlineSnapshot(` + Object { + "catalogue": Object { + "something": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + }); + + it('passes useDefaultCapabilities to registered switchers', async () => { + const getInitialCapabilities = () => ({ + catalogue: { + something: true, + }, + management: {}, + navLinks: {}, + }); + serviceSetup.registerProvider(() => getInitialCapabilities()); + + const switcher = jest.fn((_, capabilities) => capabilities); + serviceSetup.registerSwitcher(switcher); + + const result = await supertest(httpSetup.server.listener) + .post('/api/core/capabilities?useDefaultCapabilities=true') + .send({ applications: [] }) + .expect(200); + + expect(switcher).toHaveBeenCalledTimes(1); + expect(switcher).toHaveBeenCalledWith(expect.anything(), getInitialCapabilities(), true); expect(result.body).toMatchInlineSnapshot(` Object { "catalogue": Object { diff --git a/src/core/server/capabilities/resolve_capabilities.test.ts b/src/core/server/capabilities/resolve_capabilities.test.ts index 372efeff21ae2..21c723ea1ddc3 100644 --- a/src/core/server/capabilities/resolve_capabilities.test.ts +++ b/src/core/server/capabilities/resolve_capabilities.test.ts @@ -36,7 +36,7 @@ describe('resolveCapabilities', () => { }); it('returns the initial capabilities if no switcher are used', async () => { - const result = await resolveCapabilities(defaultCaps, [], request, []); + const result = await resolveCapabilities(defaultCaps, [], request, [], true); expect(result).toEqual(defaultCaps); }); @@ -55,7 +55,7 @@ describe('resolveCapabilities', () => { A: false, }, }); - const result = await resolveCapabilities(caps, [switcher], request, []); + const result = await resolveCapabilities(caps, [switcher], request, [], true); expect(result).toMatchInlineSnapshot(` Object { "catalogue": Object { @@ -83,7 +83,7 @@ describe('resolveCapabilities', () => { A: false, }, }); - await resolveCapabilities(caps, [switcher], request, []); + await resolveCapabilities(caps, [switcher], request, [], true); expect(caps.catalogue).toEqual({ A: true, B: true, @@ -105,7 +105,7 @@ describe('resolveCapabilities', () => { C: false, }, }); - const result = await resolveCapabilities(caps, [switcher], request, []); + const result = await resolveCapabilities(caps, [switcher], request, [], true); expect(result.catalogue).toEqual({ A: true, B: true, @@ -127,7 +127,7 @@ describe('resolveCapabilities', () => { .filter(([key]) => key !== 'B') .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), }); - const result = await resolveCapabilities(caps, [switcher], request, []); + const result = await resolveCapabilities(caps, [switcher], request, [], true); expect(result.catalogue).toEqual({ A: true, B: true, @@ -153,7 +153,7 @@ describe('resolveCapabilities', () => { record: false, }, }); - const result = await resolveCapabilities(caps, [switcher], request, []); + const result = await resolveCapabilities(caps, [switcher], request, [], true); expect(result.section).toEqual({ boolean: true, record: { diff --git a/src/core/server/capabilities/resolve_capabilities.ts b/src/core/server/capabilities/resolve_capabilities.ts index 1be504d4bc314..6f4eff6b882d0 100644 --- a/src/core/server/capabilities/resolve_capabilities.ts +++ b/src/core/server/capabilities/resolve_capabilities.ts @@ -23,7 +23,8 @@ import { KibanaRequest } from '../http'; export type CapabilitiesResolver = ( request: KibanaRequest, - applications: string[] + applications: string[], + useDefaultCapabilities: boolean ) => Promise; export const getCapabilitiesResolver = ( @@ -31,16 +32,24 @@ export const getCapabilitiesResolver = ( switchers: () => CapabilitiesSwitcher[] ): CapabilitiesResolver => async ( request: KibanaRequest, - applications: string[] + applications: string[], + useDefaultCapabilities: boolean ): Promise => { - return resolveCapabilities(capabilities(), switchers(), request, applications); + return resolveCapabilities( + capabilities(), + switchers(), + request, + applications, + useDefaultCapabilities + ); }; export const resolveCapabilities = async ( capabilities: Capabilities, switchers: CapabilitiesSwitcher[], request: KibanaRequest, - applications: string[] + applications: string[], + useDefaultCapabilities: boolean ): Promise => { const mergedCaps = cloneDeep({ ...capabilities, @@ -54,7 +63,7 @@ export const resolveCapabilities = async ( }); return switchers.reduce(async (caps, switcher) => { const resolvedCaps = await caps; - const changes = await switcher(request, resolvedCaps); + const changes = await switcher(request, resolvedCaps, useDefaultCapabilities); return recursiveApplyChanges(resolvedCaps, changes); }, Promise.resolve(mergedCaps)); }; diff --git a/src/core/server/capabilities/routes/resolve_capabilities.ts b/src/core/server/capabilities/routes/resolve_capabilities.ts index 3fb1bb3d13d0b..3694c4b894684 100644 --- a/src/core/server/capabilities/routes/resolve_capabilities.ts +++ b/src/core/server/capabilities/routes/resolve_capabilities.ts @@ -29,14 +29,18 @@ export function registerCapabilitiesRoutes(router: IRouter, resolver: Capabiliti authRequired: 'optional', }, validate: { + query: schema.object({ + useDefaultCapabilities: schema.boolean({ defaultValue: false }), + }), body: schema.object({ applications: schema.arrayOf(schema.string()), }), }, }, async (ctx, req, res) => { + const { useDefaultCapabilities } = req.query; const { applications } = req.body; - const capabilities = await resolver(req, applications); + const capabilities = await resolver(req, applications, useDefaultCapabilities); return res.ok({ body: capabilities, }); diff --git a/src/core/server/capabilities/types.ts b/src/core/server/capabilities/types.ts index 105233761a437..efef31dcc8417 100644 --- a/src/core/server/capabilities/types.ts +++ b/src/core/server/capabilities/types.ts @@ -34,5 +34,6 @@ export type CapabilitiesProvider = () => Partial; */ export type CapabilitiesSwitcher = ( request: KibanaRequest, - uiCapabilities: Capabilities + uiCapabilities: Capabilities, + useDefaultCapabilities: boolean ) => Partial | Promise>; diff --git a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png b/src/core/server/core_app/assets/favicons/android-chrome-192x192.png deleted file mode 100644 index 18a86e5b95c46..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/android-chrome-256x256.png b/src/core/server/core_app/assets/favicons/android-chrome-256x256.png deleted file mode 100644 index 8238d772ce40b..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-256x256.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/apple-touch-icon.png b/src/core/server/core_app/assets/favicons/apple-touch-icon.png deleted file mode 100644 index 1ffeb0852a170..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/apple-touch-icon.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/browserconfig.xml b/src/core/server/core_app/assets/favicons/browserconfig.xml deleted file mode 100644 index b3930d0f04718..0000000000000 --- a/src/core/server/core_app/assets/favicons/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #da532c - - - diff --git a/src/core/server/core_app/assets/favicons/favicon-16x16.png b/src/core/server/core_app/assets/favicons/favicon-16x16.png deleted file mode 100644 index 631f5b7c7d74b..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/favicon-16x16.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/favicon-32x32.png b/src/core/server/core_app/assets/favicons/favicon-32x32.png deleted file mode 100644 index bf94dfa995f37..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/favicon-32x32.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/favicon.distribution.png b/src/core/server/core_app/assets/favicons/favicon.distribution.png new file mode 100644 index 0000000000000..9be046aba59b6 Binary files /dev/null and b/src/core/server/core_app/assets/favicons/favicon.distribution.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon.distribution.svg b/src/core/server/core_app/assets/favicons/favicon.distribution.svg new file mode 100644 index 0000000000000..2d02461a0b8f9 --- /dev/null +++ b/src/core/server/core_app/assets/favicons/favicon.distribution.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/core/server/core_app/assets/favicons/favicon.ico b/src/core/server/core_app/assets/favicons/favicon.ico deleted file mode 100644 index db30798a6cf32..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/favicon.ico and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/favicon.png b/src/core/server/core_app/assets/favicons/favicon.png new file mode 100644 index 0000000000000..cba7a268c6c59 Binary files /dev/null and b/src/core/server/core_app/assets/favicons/favicon.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon.svg b/src/core/server/core_app/assets/favicons/favicon.svg new file mode 100644 index 0000000000000..4ae6524bf0d18 --- /dev/null +++ b/src/core/server/core_app/assets/favicons/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/core/server/core_app/assets/favicons/manifest.json b/src/core/server/core_app/assets/favicons/manifest.json deleted file mode 100644 index de65106f489b7..0000000000000 --- a/src/core/server/core_app/assets/favicons/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-256x256.png", - "sizes": "256x256", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/src/core/server/core_app/assets/favicons/mstile-150x150.png b/src/core/server/core_app/assets/favicons/mstile-150x150.png deleted file mode 100644 index 82769c1ef242b..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/mstile-150x150.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg b/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg deleted file mode 100644 index 38a64142be0b7..0000000000000 --- a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - - - - - diff --git a/src/core/server/core_app/integration_tests/static_assets.test.ts b/src/core/server/core_app/integration_tests/static_assets.test.ts index ca03c4228221f..45e7b79b5d5e6 100644 --- a/src/core/server/core_app/integration_tests/static_assets.test.ts +++ b/src/core/server/core_app/integration_tests/static_assets.test.ts @@ -34,11 +34,11 @@ describe('Platform assets', function () { }); it('exposes static assets', async () => { - await kbnTestServer.request.get(root, '/ui/favicons/favicon.ico').expect(200); + await kbnTestServer.request.get(root, '/ui/favicons/favicon.svg').expect(200); }); it('returns 404 if not found', async function () { - await kbnTestServer.request.get(root, '/ui/favicons/not-a-favicon.ico').expect(404); + await kbnTestServer.request.get(root, '/ui/favicons/not-a-favicon.svg').expect(404); }); it('does not expose folder content', async function () { diff --git a/src/core/server/core_route_handler_context.ts b/src/core/server/core_route_handler_context.ts index 520c5bd3f685b..ffb1c762b00ef 100644 --- a/src/core/server/core_route_handler_context.ts +++ b/src/core/server/core_route_handler_context.ts @@ -21,7 +21,12 @@ import { InternalCoreStart } from './internal_types'; import { KibanaRequest } from './http/router'; import { SavedObjectsClientContract } from './saved_objects/types'; -import { InternalSavedObjectsServiceStart, ISavedObjectTypeRegistry } from './saved_objects'; +import { + InternalSavedObjectsServiceStart, + ISavedObjectTypeRegistry, + ISavedObjectsExporter, + ISavedObjectsImporter, +} from './saved_objects'; import { InternalElasticsearchServiceStart, IScopedClusterClient, @@ -64,6 +69,8 @@ class CoreSavedObjectsRouteHandlerContext { ) {} #scopedSavedObjectsClient?: SavedObjectsClientContract; #typeRegistry?: ISavedObjectTypeRegistry; + #exporter?: ISavedObjectsExporter; + #importer?: ISavedObjectsImporter; public get client() { if (this.#scopedSavedObjectsClient == null) { @@ -78,6 +85,20 @@ class CoreSavedObjectsRouteHandlerContext { } return this.#typeRegistry; } + + public get exporter() { + if (this.#exporter == null) { + this.#exporter = this.savedObjectsStart.createExporter(this.client); + } + return this.#exporter; + } + + public get importer() { + if (this.#importer == null) { + this.#importer = this.savedObjectsStart.createImporter(this.client); + } + return this.#importer; + } } class CoreUiSettingsRouteHandlerContext { diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts index c8d48597fae88..7c3047ecd96e4 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -24,7 +24,6 @@ import { ISavedObjectsRepository, SavedObjectsImportOptions, SavedObjectsResolveImportErrorsOptions, - SavedObjectsExportOptions, KibanaRequest, IBasePath, } from '..'; @@ -40,8 +39,10 @@ export type IncrementSavedObjectsImportOptions = BaseIncrementOptions & export type IncrementSavedObjectsResolveImportErrorsOptions = BaseIncrementOptions & Pick; /** @internal */ -export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & - Pick & { supportedTypes: string[] }; +export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & { + types?: string[]; + supportedTypes: string[]; +}; export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate'; export const BULK_GET_STATS_PREFIX = 'apiCalls.savedObjectsBulkGet'; diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 9b667f888771e..4545396c27b5e 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -24,6 +24,12 @@ Object { } `; +exports[`accepts valid hostnames 5`] = ` +Object { + "host": "0.0.0.0", +} +`; + exports[`basePath throws if appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; exports[`basePath throws if is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`; @@ -105,6 +111,8 @@ Object { exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; +exports[`throws if invalid hostname 2`] = `"[host]: value 0 is not a valid hostname (use \\"0.0.0.0\\" to bind to all interfaces)"`; + exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; exports[`with compression accepts valid referrer whitelist 1`] = ` @@ -113,6 +121,7 @@ Array [ "8.8.8.8", "::1", "localhost", + "0.0.0.0", ] `; diff --git a/src/core/server/http/base_path_proxy_server.test.ts b/src/core/server/http/base_path_proxy_server.test.ts new file mode 100644 index 0000000000000..9f4ffdcf8e081 --- /dev/null +++ b/src/core/server/http/base_path_proxy_server.test.ts @@ -0,0 +1,1052 @@ +/* + * 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 { BasePathProxyServer, BasePathProxyServerOptions } from './base_path_proxy_server'; +import { loggingSystemMock } from '../logging/logging_system.mock'; +import { DevConfig } from '../dev/dev_config'; +import { EMPTY } from 'rxjs'; +import { HttpConfig } from './http_config'; +import { ByteSizeValue, schema } from '@kbn/config-schema'; +import { + KibanaRequest, + KibanaResponseFactory, + Router, + RouteValidationFunction, + RouteValidationResultFactory, +} from './router'; +import { HttpServer } from './http_server'; +import supertest from 'supertest'; +import { RequestHandlerContext } from 'kibana/server'; +import { readFileSync } from 'fs'; +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { omit } from 'lodash'; +import { Readable } from 'stream'; + +/** + * Most of these tests are inspired by: + * src/core/server/http/http_server.test.ts + * and copied for completeness from that file. The modifications are that these tests use the developer proxy. + */ +describe('BasePathProxyServer', () => { + let server: HttpServer; + let proxyServer: BasePathProxyServer; + let config: HttpConfig; + let configWithSSL: HttpConfig; + let basePath: string; + let certificate: string; + let key: string; + let proxySupertest: supertest.SuperTest; + const logger = loggingSystemMock.createLogger(); + const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); + + beforeAll(() => { + certificate = readFileSync(KBN_CERT_PATH, 'utf8'); + key = readFileSync(KBN_KEY_PATH, 'utf8'); + }); + + beforeEach(async () => { + // setup the server but don't start it until each individual test so that routes can be dynamically configured per unit test. + server = new HttpServer(logger, 'tests'); + config = ({ + name: 'kibana', + host: '127.0.0.1', + port: 10012, + compression: { enabled: true }, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + autoListen: true, + keepaliveTimeout: 1000, + socketTimeout: 1000, + cors: { + enabled: false, + allowCredentials: false, + allowOrigin: [], + }, + ssl: { enabled: false }, + customResponseHeaders: {}, + maxPayload: new ByteSizeValue(1024), + rewriteBasePath: true, + } as unknown) as HttpConfig; + + configWithSSL = { + ...config, + ssl: { + enabled: true, + certificate, + cipherSuites: ['TLS_AES_256_GCM_SHA384'], + getSecureOptions: () => 0, + key, + redirectHttpFromPort: config.port + 1, + }, + } as HttpConfig; + + // setup and start the proxy server + const proxyConfig: HttpConfig = { ...config, port: 10013 }; + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServer = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => true, + delayUntil: () => EMPTY, + }; + await proxyServer.start(options); + + // set the base path or throw if for some unknown reason it is not setup + if (proxyServer.basePath == null) { + throw new Error('Invalid null base path, all tests will fail'); + } else { + basePath = proxyServer.basePath; + } + proxySupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await server.stop(); + await proxyServer.stop(); + jest.clearAllMocks(); + }); + + test('root URL will return a 302 redirect', async () => { + await proxySupertest.get('/').expect(302); + }); + + test('root URL will return a redirect location with exactly 3 characters that are a-z', async () => { + const res = await proxySupertest.get('/'); + const location = res.header.location; + expect(location).toMatch(/[a-z]{3}/); + }); + + test('valid params', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + router.get( + { + path: '/{test}', + validate: { + params: schema.object({ + test: schema.string(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.params.test }); + } + ); + const { registerRouter } = await server.setup(config); + registerRouter(router); + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/some-string`) + .expect(200) + .then((res) => { + expect(res.text).toBe('some-string'); + }); + }); + + test('invalid params', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.get( + { + path: '/{test}', + validate: { + params: schema.object({ + test: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: String(req.params.test) }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/some-string`) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: 'Bad Request', + statusCode: 400, + message: '[request params.test]: expected value of type [number] but got [string]', + }); + }); + }); + + test('valid query', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.get( + { + path: '/', + validate: { + query: schema.object({ + bar: schema.string(), + quux: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.query }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/?bar=test&quux=123`) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', quux: 123 }); + }); + }); + + test('invalid query', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.get( + { + path: '/', + validate: { + query: schema.object({ + bar: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.query }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/?bar=test`) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: 'Bad Request', + statusCode: 400, + message: '[request query.bar]: expected value of type [number] but got [string]', + }); + }); + }); + + test('valid body', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + bar: schema.string(), + baz: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('valid body with validate function', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('not inline validation - specifying params', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + const bodyValidation = ( + { bar, baz }: any = {}, + { ok, badRequest }: RouteValidationResultFactory + ) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }; + + router.post( + { + path: '/', + validate: { + body: bodyValidation, + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('not inline validation - specifying validation handler', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + const bodyValidation: RouteValidationFunction<{ bar: string; baz: number }> = ( + { bar, baz } = {}, + { ok, badRequest } + ) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }; + + router.post( + { + path: '/', + validate: { + body: bodyValidation, + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('not inline handler - KibanaRequest', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + const handler = ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ) => { + const body = { + bar: req.body.bar.toUpperCase(), + baz: req.body.baz.toString(), + }; + + return res.ok({ body }); + }; + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + handler + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'TEST', baz: '123' }); + }); + }); + + test('invalid body', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + bar: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ bar: 'test' }) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: 'Bad Request', + statusCode: 400, + message: '[request body.bar]: expected value of type [number] but got [string]', + }); + }); + }); + + test('handles putting', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.put( + { + path: '/', + validate: { + body: schema.object({ + key: schema.string(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .put(`${basePath}/foo/`) + .send({ key: 'new value' }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ key: 'new value' }); + }); + }); + + test('handles deleting', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.delete( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: { key: req.params.id } }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .delete(`${basePath}/foo/3`) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ key: 3 }); + }); + }); + + describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { + let configWithBasePath: HttpConfig; + + beforeEach(async () => { + configWithBasePath = { + ...config, + basePath: '/bar', + rewriteBasePath: false, + } as HttpConfig; + + const router = new Router(`${basePath}/`, logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (_, __, res) => res.ok({ body: 'value:/' })); + router.get({ path: '/foo', validate: false }, (_, __, res) => res.ok({ body: 'value:/foo' })); + + const { registerRouter } = await server.setup(configWithBasePath); + registerRouter(router); + + await server.start(); + }); + + test('/bar => 404', async () => { + await proxySupertest.get(`${basePath}/bar`).expect(404); + }); + + test('/bar/ => 404', async () => { + await proxySupertest.get(`${basePath}/bar/`).expect(404); + }); + + test('/bar/foo => 404', async () => { + await proxySupertest.get(`${basePath}/bar/foo`).expect(404); + }); + + test('/ => /', async () => { + await proxySupertest + .get(`${basePath}/`) + .expect(200) + .then((res) => { + expect(res.text).toBe('value:/'); + }); + }); + + test('/foo => /foo', async () => { + await proxySupertest + .get(`${basePath}/foo`) + .expect(200) + .then((res) => { + expect(res.text).toBe('value:/foo'); + }); + }); + }); + + test('with defined `redirectHttpFromPort`', async () => { + const router = new Router(`${basePath}/`, logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (_, __, res) => res.ok({ body: 'value:/' })); + + const { registerRouter } = await server.setup(configWithSSL); + registerRouter(router); + + await server.start(); + }); + + test('allows attaching metadata to attach meta-data tag strings to a route', async () => { + const tags = ['my:tag']; + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get({ path: '/with-tags', validate: false, options: { tags } }, (_, req, res) => + res.ok({ body: { tags: req.route.options.tags } }) + ); + router.get({ path: '/without-tags', validate: false }, (_, req, res) => + res.ok({ body: { tags: req.route.options.tags } }) + ); + registerRouter(router); + + await server.start(); + await proxySupertest.get(`${basePath}/with-tags`).expect(200, { tags }); + + await proxySupertest.get(`${basePath}/without-tags`).expect(200, { tags: [] }); + }); + + describe('response headers', () => { + test('default headers', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (_, req, res) => res.ok({ body: req.route })); + registerRouter(router); + + await server.start(); + const response = await proxySupertest.get(`${basePath}/`).expect(200); + + const restHeaders = omit(response.header, ['date', 'content-length']); + expect(restHeaders).toMatchInlineSnapshot(` + Object { + "accept-ranges": "bytes", + "cache-control": "private, no-cache, no-store, must-revalidate", + "connection": "close", + "content-type": "application/json; charset=utf-8", + } + `); + }); + }); + + test('exposes route details of incoming request to a route handler (POST + payload options)', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.object({ test: schema.number() }) }, + options: { body: { accepts: 'application/json' } }, + }, + (_, req, res) => res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + await proxySupertest + .post(`${basePath}/`) + .send({ test: 1 }) + .expect(200, { + method: 'post', + path: `${basePath}/`, + options: { + authRequired: true, + xsrfRequired: true, + tags: [], + timeout: { + payload: 10000, + idleSocket: 1000, + }, + body: { + parse: true, // hapi populates the default + maxBytes: 1024, // hapi populates the default + accepts: ['application/json'], + output: 'data', + }, + }, + }); + }); + + test('should return a stream in the body', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.put( + { + path: '/', + validate: { body: schema.stream() }, + options: { body: { output: 'stream' } }, + }, + (_, req, res) => { + try { + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + + await server.start(); + await proxySupertest.put(`${basePath}/`).send({ test: 1 }).expect(200, { + parse: true, + maxBytes: 1024, // hapi populates the default + output: 'stream', + }); + }); + + describe('timeout options', () => { + describe('payload timeout', () => { + test('POST routes set the payload timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.post( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (_, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest + .post(`${basePath}/`) + .send({ test: 1 }) + .expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + + test('DELETE routes set the payload timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.delete( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (context, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest.delete(`${basePath}/`).expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + + test('PUT routes set the payload timeout and automatically adjusts the idle socket timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.put( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (_, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest.put(`${basePath}/`).expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + + test('PATCH routes set the payload timeout and automatically adjusts the idle socket timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.patch( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (_, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest.patch(`${basePath}/`).expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + }); + + describe('idleSocket timeout', () => { + test('uses server socket timeout when not specified in the route', async () => { + const { registerRouter } = await server.setup({ + ...config, + socketTimeout: 11000, + }); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get( + { + path: '/', + validate: { body: schema.maybe(schema.any()) }, + }, + (_, req, res) => { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } + ); + registerRouter(router); + + await server.start(); + await proxySupertest + .get(`${basePath}/`) + .send() + .expect(200, { + timeout: { + idleSocket: 11000, + }, + }); + }); + + test('sets the socket timeout when specified in the route', async () => { + const { registerRouter } = await server.setup({ + ...config, + socketTimeout: 11000, + }); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get( + { + path: '/', + validate: { body: schema.maybe(schema.any()) }, + options: { timeout: { idleSocket: 12000 } }, + }, + (context, req, res) => { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } + ); + registerRouter(router); + + await server.start(); + await proxySupertest + .get(`${basePath}/`) + .send() + .expect(200, { + timeout: { + idleSocket: 12000, + }, + }); + }); + + test('idleSocket timeout can be smaller than the payload timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.post( + { + path: `${basePath}/`, + validate: { body: schema.any() }, + options: { + timeout: { + payload: 1000, + idleSocket: 10, + }, + }, + }, + (_, req, res) => { + return res.ok({ body: { timeout: req.route.options.timeout } }); + } + ); + + registerRouter(router); + + await server.start(); + }); + }); + }); + + describe('shouldRedirect', () => { + let proxyServerWithoutShouldRedirect: BasePathProxyServer; + let proxyWithoutShouldRedirectSupertest: supertest.SuperTest; + + beforeEach(async () => { + // setup and start a proxy server which does not use "shouldRedirectFromOldBasePath" + const proxyConfig: HttpConfig = { ...config, port: 10004 }; + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServerWithoutShouldRedirect = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => false, // Return false to not redirect + delayUntil: () => EMPTY, + }; + await proxyServerWithoutShouldRedirect.start(options); + proxyWithoutShouldRedirectSupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await proxyServerWithoutShouldRedirect.stop(); + }); + + test('it will do a redirect if it detects what looks like a stale or previously used base path', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + const res = await proxySupertest.get(`/${fakeBasePath}`).expect(302); + const location = res.header.location; + expect(location).toEqual(`${basePath}/`); + }); + + test('it will NOT do a redirect if it detects what looks like a stale or previously used base path if we intentionally turn it off', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + await proxyWithoutShouldRedirectSupertest.get(`/${fakeBasePath}`).expect(404); + }); + + test('it will NOT redirect if it detects a larger path than 3 characters', async () => { + await proxySupertest.get('/abcde').expect(404); + }); + + test('it will NOT redirect if it is not a GET verb', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + await proxySupertest.put(`/${fakeBasePath}`).expect(404); + }); + }); + + describe('constructor option for sending in a custom basePath', () => { + let proxyServerWithFooBasePath: BasePathProxyServer; + let proxyWithFooBasePath: supertest.SuperTest; + + beforeEach(async () => { + // setup and start a proxy server which uses a basePath of "foo" + const proxyConfig: HttpConfig = { ...config, port: 10004, basePath: '/foo' }; // <-- "foo" here in basePath + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServerWithFooBasePath = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => true, + delayUntil: () => EMPTY, + }; + await proxyServerWithFooBasePath.start(options); + proxyWithFooBasePath = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await proxyServerWithFooBasePath.stop(); + }); + + test('it will do a redirect to foo which is our passed in value for the configuration', async () => { + const res = await proxyWithFooBasePath.get('/bar').expect(302); + const location = res.header.location; + expect(location).toEqual('/foo/'); + }); + }); +}); diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index d461abe54ccbd..dfcd0757c2d1e 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -143,12 +143,25 @@ export class BasePathProxyServer { handler: { proxy: { agent: this.httpsAgent, - host: this.server.info.host, passThrough: true, - port: this.devConfig.basePathProxyTargetPort, - // typings mismatch. h2o2 doesn't support "socket" - protocol: this.server.info.protocol as HapiProxy.ProxyHandlerOptions['protocol'], xforward: true, + mapUri: async (request) => { + return { + // Passing in this header to merge it is a workaround until this is fixed: + // https://github.com/hapijs/h2o2/issues/124 + headers: + request.headers['content-length'] != null + ? { 'content-length': request.headers['content-length'] } + : undefined, + uri: Url.format({ + hostname: request.server.info.host, + port: this.devConfig.basePathProxyTargetPort, + protocol: request.server.info.protocol, + pathname: request.path, + query: request.query, + }), + }; + }, }, }, method: '*', diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 1ff0670d78f4e..40bca89c21cb3 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -19,8 +19,6 @@ import { Request, Server } from '@hapi/hapi'; import hapiAuthCookie from '@hapi/cookie'; -// @ts-expect-error no TS definitions -import Statehood from '@hapi/statehood'; import { KibanaRequest, ensureRawRequest } from './router'; import { SessionStorageFactory, SessionStorage } from './session_storage'; @@ -148,7 +146,7 @@ export async function createCookieSessionStorageFactory( path: basePath === undefined ? '/' : basePath, clearInvalid: false, isHttpOnly: true, - isSameSite: cookieOptions.sameSite === 'None' ? false : cookieOptions.sameSite ?? false, + isSameSite: cookieOptions.sameSite ?? false, }, validateFunc: async (req: Request, session: T | T[]) => { const result = cookieOptions.validate(session); @@ -159,23 +157,6 @@ export async function createCookieSessionStorageFactory( }, }); - // A hack to support SameSite: 'None'. - // Remove it after update Hapi to v19 that supports SameSite: 'None' out of the box. - if (cookieOptions.sameSite === 'None') { - log.debug('Patching Statehood.prepareValue'); - const originalPrepareValue = Statehood.prepareValue; - Statehood.prepareValue = function kibanaStatehoodPrepareValueWrapper( - name: string, - value: unknown, - options: any - ) { - if (name === cookieOptions.name) { - options.isSameSite = cookieOptions.sameSite; - } - return originalPrepareValue(name, value, options); - }; - } - return { asScoped(request: KibanaRequest) { return new ScopedCookieSessionStorage(log, server, ensureRawRequest(request)); diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index b71763e8a2e14..b1b2ba5b295a7 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -22,8 +22,8 @@ import { config, HttpConfig } from './http_config'; import { CspConfig } from '../csp'; import { ExternalUrlConfig } from '../external_url'; -const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; -const invalidHostname = 'asdf$%^'; +const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost', '0.0.0.0']; +const invalidHostnames = ['asdf$%^', '0']; jest.mock('os', () => { const original = jest.requireActual('os'); @@ -48,11 +48,10 @@ test('accepts valid hostnames', () => { }); test('throws if invalid hostname', () => { - const httpSchema = config.schema; - const obj = { - host: invalidHostname, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + for (const host of invalidHostnames) { + const httpSchema = config.schema; + expect(() => httpSchema.validate({ host })).toThrowErrorMatchingSnapshot(); + } }); describe('requestId', () => { @@ -304,9 +303,9 @@ describe('with compression', () => { test('throws if invalid referrer whitelist', () => { const httpSchema = config.schema; - const invalidHostnames = { + const nonEmptyArray = { compression: { - referrerWhitelist: [invalidHostname], + referrerWhitelist: invalidHostnames, }, }; const emptyArray = { @@ -314,7 +313,7 @@ describe('with compression', () => { referrerWhitelist: [], }, }; - expect(() => httpSchema.validate(invalidHostnames)).toThrowErrorMatchingSnapshot(); + expect(() => httpSchema.validate(nonEmptyArray)).toThrowErrorMatchingSnapshot(); expect(() => httpSchema.validate(emptyArray)).toThrowErrorMatchingSnapshot(); }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 2bd296fe338ab..aa4db6f88d338 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -73,6 +73,11 @@ export const config = { host: schema.string({ defaultValue: 'localhost', hostname: true, + validate(value) { + if (value === '0') { + return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; + } + }, }), maxPayload: schema.byteSize({ defaultValue: '1048576b', diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index cbb60480c4cf1..70c346a5333cc 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -888,52 +888,48 @@ describe('conditional compression', () => { expect(response.header).not.toHaveProperty('content-encoding'); }); }); +}); - describe('response headers', () => { - it('allows to configure "keep-alive" header', async () => { - const { registerRouter, server: innerServer } = await server.setup({ - ...config, - keepaliveTimeout: 100_000, - }); +describe('response headers', () => { + test('allows to configure "keep-alive" header', async () => { + const { registerRouter, server: innerServer } = await server.setup({ + ...config, + keepaliveTimeout: 100_000, + }); - const router = new Router('', logger, enhanceWithContext); - router.get({ path: '/', validate: false }, (context, req, res) => - res.ok({ body: req.route }) - ); - registerRouter(router); + const router = new Router('', logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: req.route })); + registerRouter(router); - await server.start(); - const response = await supertest(innerServer.listener) - .get('/') - .set('Connection', 'keep-alive') - .expect(200); + await server.start(); + const response = await supertest(innerServer.listener) + .get('/') + .set('Connection', 'keep-alive') + .expect(200); - expect(response.header.connection).toBe('keep-alive'); - expect(response.header['keep-alive']).toBe('timeout=100'); - }); + expect(response.header.connection).toBe('keep-alive'); + expect(response.header['keep-alive']).toBe('timeout=100'); + }); - it('default headers', async () => { - const { registerRouter, server: innerServer } = await server.setup(config); + test('default headers', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); - const router = new Router('', logger, enhanceWithContext); - router.get({ path: '/', validate: false }, (context, req, res) => - res.ok({ body: req.route }) - ); - registerRouter(router); + const router = new Router('', logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: req.route })); + registerRouter(router); - await server.start(); - const response = await supertest(innerServer.listener).get('/').expect(200); - - const restHeaders = omit(response.header, ['date', 'content-length']); - expect(restHeaders).toMatchInlineSnapshot(` - Object { - "accept-ranges": "bytes", - "cache-control": "private, no-cache, no-store, must-revalidate", - "connection": "close", - "content-type": "application/json; charset=utf-8", - } - `); - }); + await server.start(); + const response = await supertest(innerServer.listener).get('/').expect(200); + + const restHeaders = omit(response.header, ['date', 'content-length']); + expect(restHeaders).toMatchInlineSnapshot(` + Object { + "accept-ranges": "bytes", + "cache-control": "private, no-cache, no-store, must-revalidate", + "connection": "close", + "content-type": "application/json; charset=utf-8", + } + `); }); }); @@ -1270,31 +1266,31 @@ describe('timeout options', () => { }, }); }); - }); - test(`idleSocket timeout can be smaller than the payload timeout`, async () => { - const { registerRouter } = await server.setup(config); + test('idleSocket timeout can be smaller than the payload timeout', async () => { + const { registerRouter } = await server.setup(config); - const router = new Router('', logger, enhanceWithContext); - router.post( - { - path: '/', - validate: { body: schema.any() }, - options: { - timeout: { - payload: 1000, - idleSocket: 10, + const router = new Router('', logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.any() }, + options: { + timeout: { + payload: 1000, + idleSocket: 10, + }, }, }, - }, - (context, req, res) => { - return res.ok({ body: { timeout: req.route.options.timeout } }); - } - ); + (context, req, res) => { + return res.ok({ body: { timeout: req.route.options.timeout } }); + } + ); - registerRouter(router); + registerRouter(router); - await server.start(); + await server.start(); + }); }); }); @@ -1329,13 +1325,14 @@ test('should return a stream in the body', async () => { describe('setup contract', () => { describe('#createSessionStorage', () => { - it('creates session storage factory', async () => { + test('creates session storage factory', async () => { const { createCookieSessionStorageFactory } = await server.setup(config); const sessionStorageFactory = await createCookieSessionStorageFactory(cookieOptions); expect(sessionStorageFactory.asScoped).toBeDefined(); }); - it('creates session storage factory only once', async () => { + + test('creates session storage factory only once', async () => { const { createCookieSessionStorageFactory } = await server.setup(config); const create = async () => await createCookieSessionStorageFactory(cookieOptions); @@ -1343,7 +1340,7 @@ describe('setup contract', () => { expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created'); }); - it('does not throw if called after stop', async () => { + test('does not throw if called after stop', async () => { const { createCookieSessionStorageFactory } = await server.setup(config); await server.stop(); expect(() => { @@ -1353,7 +1350,7 @@ describe('setup contract', () => { }); describe('#getServerInfo', () => { - it('returns correct information', async () => { + test('returns correct information', async () => { let { getServerInfo } = await server.setup(config); expect(getServerInfo()).toEqual({ @@ -1378,7 +1375,7 @@ describe('setup contract', () => { }); }); - it('returns correct protocol when ssl is enabled', async () => { + test('returns correct protocol when ssl is enabled', async () => { const { getServerInfo } = await server.setup(configWithSSL); expect(getServerInfo().protocol).toEqual('https'); @@ -1386,7 +1383,7 @@ describe('setup contract', () => { }); describe('#registerStaticDir', () => { - it('does not throw if called after stop', async () => { + test('does not throw if called after stop', async () => { const { registerStaticDir } = await server.setup(config); await server.stop(); expect(() => { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 42e89b66d9c51..81f7c9c45ba50 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Server, ServerRoute } from '@hapi/hapi'; +import { Server } from '@hapi/hapi'; import HapiStaticFiles from '@hapi/inert'; import url from 'url'; import uuid from 'uuid'; @@ -167,6 +167,8 @@ export class HttpServer { for (const router of this.registeredRouters) { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); + // Hapi does not allow payload validation to be specified for 'head' or 'get' requests + const validate = isSafeMethod(route.method) ? undefined : { payload: true }; const { authRequired, tags, body = {}, timeout } = route.options; const { accepts: allow, maxBytes, output, parse } = body; @@ -174,7 +176,7 @@ export class HttpServer { xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method), }; - const routeOpts: ServerRoute = { + this.server.route({ handler: route.handler, method: route.method, path: route.path, @@ -182,6 +184,11 @@ export class HttpServer { auth: this.getAuthOption(authRequired), app: kibanaRouteOptions, tags: tags ? Array.from(tags) : undefined, + // TODO: This 'validate' section can be removed once the legacy platform is completely removed. + // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default + // validation applied in ./http_tools#getServerOptions + // (All NP routes are already required to specify their own validation in order to access the payload) + validate, // @ts-expect-error Types are outdated and doesn't allow `payload.multipart` to be `true` payload: [allow, maxBytes, output, parse, timeout?.payload].some((x) => x !== undefined) ? { @@ -197,22 +204,7 @@ export class HttpServer { socket: timeout?.idleSocket ?? this.config!.socketTimeout, }, }, - }; - - // Hapi does not allow payload validation to be specified for 'head' or 'get' requests - if (!isSafeMethod(route.method)) { - // TODO: This 'validate' section can be removed once the legacy platform is completely removed. - // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default - // validation applied in ./http_tools#getServerOptions - // (All NP routes are already required to specify their own validation in order to access the payload) - // TODO: Move the setting of the validate option back up to being set at `routeOpts` creation-time once - // https://github.com/hapijs/hoek/pull/365 is merged and released in @hapi/hoek v9.1.1. At that point I - // imagine the ts-error below will go away as well. - // @ts-expect-error "Property 'validate' does not exist on type 'RouteOptions'" <-- ehh?!? yes it does! - routeOpts.options!.validate = { payload: true }; - } - - this.server.route(routeOpts); + }); } } diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 8bec26f31fa26..f09f3dc2730a1 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -29,8 +29,8 @@ import Hoek from '@hapi/hoek'; import type { ServerOptions as TLSOptions } from 'https'; import type { ValidationError } from 'joi'; import uuid from 'uuid'; +import { ensureNoUnsafeProperties } from '@kbn/std'; import { HttpConfig } from './http_config'; -import { validateObject } from './prototype_pollution'; const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf']; /** @@ -69,7 +69,7 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { // This is a default payload validation which applies to all LP routes which do not specify their own // `validate.payload` handler, in order to reduce the likelyhood of prototype pollution vulnerabilities. // (All NP routes are already required to specify their own validation in order to access the payload) - payload: (value) => Promise.resolve(validateObject(value)), + payload: (value) => Promise.resolve(ensureNoUnsafeProperties(value)), }, }, state: { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0f2761b67437d..0dae17b4c211e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -58,6 +58,8 @@ import { ISavedObjectTypeRegistry, SavedObjectsServiceSetup, SavedObjectsServiceStart, + ISavedObjectsExporter, + ISavedObjectsImporter, } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; @@ -265,13 +267,12 @@ export { SavedObjectsClientFactoryProvider, SavedObjectsCreateOptions, SavedObjectsErrorHelpers, - SavedObjectsExportOptions, SavedObjectsExportResultDetails, SavedObjectsFindResult, SavedObjectsFindResponse, SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportMissingReferencesError, SavedObjectsImportOptions, SavedObjectsImportResponse, @@ -317,9 +318,15 @@ export { SavedObjectMigrationMap, SavedObjectMigrationFn, SavedObjectsUtils, - exportSavedObjectsToStream, - importSavedObjectsFromStream, - resolveSavedObjectsImportErrors, + SavedObjectsExporter, + ISavedObjectsExporter, + SavedObjectExportBaseOptions, + SavedObjectsExportByObjectOptions, + SavedObjectsExportByTypeOptions, + SavedObjectsExportError, + SavedObjectsImporter, + ISavedObjectsImporter, + SavedObjectsImportError, } from './saved_objects'; export { @@ -399,6 +406,8 @@ export interface RequestHandlerContext { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; + exporter: ISavedObjectsExporter; + importer: ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 669286ccb2318..609555e4e34c1 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -211,6 +211,8 @@ export class LegacyService implements CoreService { createScopedRepository: startDeps.core.savedObjects.createScopedRepository, createInternalRepository: startDeps.core.savedObjects.createInternalRepository, createSerializer: startDeps.core.savedObjects.createSerializer, + createExporter: startDeps.core.savedObjects.createExporter, + createImporter: startDeps.core.savedObjects.createImporter, getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, }, metrics: { @@ -265,7 +267,6 @@ export class LegacyService implements CoreService { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, registerType: setupDeps.core.savedObjects.registerType, - getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, status: { isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, diff --git a/src/core/server/logging/layouts/json_layout.ts b/src/core/server/logging/layouts/json_layout.ts index 7573d0b837416..34c3c325e7328 100644 --- a/src/core/server/logging/layouts/json_layout.ts +++ b/src/core/server/logging/layouts/json_layout.ts @@ -18,7 +18,7 @@ */ import moment from 'moment-timezone'; -import { merge } from 'lodash'; +import { merge } from '@kbn/std'; import { schema } from '@kbn/config-schema'; import { LogRecord, Layout } from '@kbn/logging'; @@ -53,22 +53,19 @@ export class JsonLayout implements Layout { } public format(record: LogRecord): string { - return JSON.stringify( - merge( - { - '@timestamp': moment(record.timestamp).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), - message: record.message, - error: JsonLayout.errorToSerializableObject(record.error), - log: { - level: record.level.id.toUpperCase(), - logger: record.context, - }, - process: { - pid: record.pid, - }, - }, - record.meta - ) - ); + const log = { + '@timestamp': moment(record.timestamp).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + message: record.message, + error: JsonLayout.errorToSerializableObject(record.error), + log: { + level: record.level.id.toUpperCase(), + logger: record.context, + }, + process: { + pid: record.pid, + }, + }; + const output = record.meta ? merge(log, record.meta) : log; + return JSON.stringify(output); } } diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 03a0ae2d6443a..c4f0cea428ea5 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -203,6 +203,8 @@ function createCoreRequestHandlerContextMock() { savedObjects: { client: savedObjectsClientMock.create(), typeRegistry: savedObjectsTypeRegistryMock.create(), + exporter: savedObjectsServiceMock.createExporter(), + importer: savedObjectsServiceMock.createImporter(), }, elasticsearch: { client: elasticsearchServiceMock.createScopedClusterClient(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 3b2634ddbe315..42f44e4405443 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -188,7 +188,6 @@ export function createPluginSetupContext( setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, registerType: deps.savedObjects.registerType, - getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, }, status: { core$: deps.status.core$, @@ -241,6 +240,8 @@ export function createPluginStartContext( createInternalRepository: deps.savedObjects.createInternalRepository, createScopedRepository: deps.savedObjects.createScopedRepository, createSerializer: deps.savedObjects.createSerializer, + createExporter: deps.savedObjects.createExporter, + createImporter: deps.savedObjects.createImporter, getTypeRegistry: deps.savedObjects.getTypeRegistry, }, metrics: { diff --git a/src/core/server/rendering/views/template.tsx b/src/core/server/rendering/views/template.tsx index 76af229ac02ba..e4787ee26e12c 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -76,33 +76,11 @@ export const Template: FunctionComponent = ({ Elastic - {/* Favicons (generated from http://realfavicongenerator.net/) */} - - - - - - - + {/* The alternate icon is a fallback for Safari which does not yet support SVG favicons */} + + + {/* Inject stylesheets into the before scripts so that KP plugins with bundled styles will override them */} diff --git a/src/core/server/saved_objects/export/errors.ts b/src/core/server/saved_objects/export/errors.ts new file mode 100644 index 0000000000000..3a26b092ab489 --- /dev/null +++ b/src/core/server/saved_objects/export/errors.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObject } from '../../../types'; + +/** + * @public + */ +export class SavedObjectsExportError extends Error { + constructor( + public readonly type: string, + message: string, + public readonly attributes?: Record + ) { + super(message); + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, SavedObjectsExportError.prototype); + } + + static exportSizeExceeded(limit: number) { + return new SavedObjectsExportError( + 'export-size-exceeded', + `Can't export more than ${limit} objects` + ); + } + + static objectFetchError(objects: SavedObject[]) { + return new SavedObjectsExportError('object-fetch-error', 'Error fetching objects to export', { + objects, + }); + } +} diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/fetch_nested_dependencies.test.ts similarity index 99% rename from src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts rename to src/core/server/saved_objects/export/fetch_nested_dependencies.test.ts index 862d11cfa663a..62ee402c4da92 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/fetch_nested_dependencies.test.ts @@ -19,7 +19,7 @@ import { SavedObject } from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { getObjectReferencesToFetch, fetchNestedDependencies } from './inject_nested_depdendencies'; +import { getObjectReferencesToFetch, fetchNestedDependencies } from './fetch_nested_dependencies'; import { SavedObjectsErrorHelpers } from '..'; describe('getObjectReferencesToFetch()', () => { diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts b/src/core/server/saved_objects/export/fetch_nested_dependencies.ts similarity index 100% rename from src/core/server/saved_objects/export/inject_nested_depdendencies.ts rename to src/core/server/saved_objects/export/fetch_nested_dependencies.ts diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts deleted file mode 100644 index 8f397c01ffa71..0000000000000 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ /dev/null @@ -1,955 +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 { exportSavedObjectsToStream } from './get_sorted_objects_for_export'; -import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; -import { Readable } from 'stream'; -import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; - -async function readStreamToCompletion(stream: Readable) { - return createPromiseFromStreams([stream, createConcatStream([])]); -} - -describe('getSortedObjectsForExport()', () => { - const savedObjectsClient = savedObjectsClientMock.create(); - - afterEach(() => { - savedObjectsClient.find.mockReset(); - savedObjectsClient.bulkGet.mockReset(); - savedObjectsClient.create.mockReset(); - savedObjectsClient.bulkCreate.mockReset(); - savedObjectsClient.delete.mockReset(); - savedObjectsClient.get.mockReset(); - savedObjectsClient.update.mockReset(); - }); - - test('exports selected types and sorts them', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('omits the `namespaces` property from the export', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - namespaces: ['foo', 'bar'], - score: 0, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - namespaces: ['foo', 'bar'], - score: 0, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('exclude export details if option is specified', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - excludeExportDetails: true, - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] - `); - }); - - test('exports selected types with search string when present', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - search: 'foo', - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": "foo", - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('exports selected types with references when present', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 1, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - hasReference: [ - { - id: '1', - type: 'index-pattern', - }, - ], - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 1, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": Array [ - Object { - "id": "1", - "type": "index-pattern", - }, - ], - "hasReferenceOperator": "OR", - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('exports from the provided namespace when present', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - namespace: 'foo', - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": Array [ - "foo", - ], - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('export selected types throws error when exceeding exportSizeLimit', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - type: 'index-pattern', - name: 'name', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - await expect( - exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 1, - types: ['index-pattern', 'search'], - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`); - }); - - test('sorts objects within type', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 3, - per_page: 10000, - page: 1, - saved_objects: [ - { - id: '3', - type: 'index-pattern', - attributes: { - name: 'baz', - }, - score: 1, - references: [], - }, - { - id: '1', - type: 'index-pattern', - attributes: { - name: 'foo', - }, - score: 1, - references: [], - }, - { - id: '2', - type: 'index-pattern', - attributes: { - name: 'bar', - }, - score: 1, - references: [], - }, - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - types: ['index-pattern'], - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object { - "name": "foo", - }, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "bar", - }, - "id": "2", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "baz", - }, - "id": "3", - "references": Array [], - "type": "index-pattern", - }, - Object { - "exportedCount": 3, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - }); - - test('exports selected objects and sorts them', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - references: [ - { - id: '1', - name: 'name', - type: 'index-pattern', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - references: [], - }, - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - objects: [ - { - type: 'index-pattern', - id: '1', - }, - { - type: 'search', - id: '2', - }, - ], - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "id": "1", - "type": "index-pattern", - }, - Object { - "id": "2", - "type": "search", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('modifies return results to redact `namespaces` attribute', async () => { - const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), - createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), - createSavedObject({ type: 'other', id: '3' }), - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - objects: [ - { type: 'multi', id: '1' }, - { type: 'multi', id: '2' }, - { type: 'other', id: '3' }, - ], - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toEqual([ - createSavedObject({ type: 'multi', id: '1' }), - createSavedObject({ type: 'multi', id: '2' }), - createSavedObject({ type: 'other', id: '3' }), - expect.objectContaining({ exportedCount: 3 }), - ]); - }); - - test('includes nested dependencies when passed in', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - references: [ - { - type: 'index-pattern', - name: 'name', - id: '1', - }, - ], - }, - ], - }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'index-pattern', - attributes: {}, - references: [], - }, - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - objects: [ - { - type: 'search', - id: '2', - }, - ], - includeReferencesDeep: true, - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "id": "2", - "type": "search", - }, - ], - Object { - "namespace": undefined, - }, - ], - Array [ - Array [ - Object { - "id": "1", - "type": "index-pattern", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('export selected objects throws error when exceeding exportSizeLimit', async () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - objects: [ - { - type: 'index-pattern', - id: '1', - }, - { - type: 'search', - id: '2', - }, - ], - }; - await expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Can't export more than 1 objects"` - ); - }); - - test('rejects when neither type nor objects paramaters are passed in', () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - types: undefined, - objects: undefined, - }; - - expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Either \`type\` or \`objects\` are required."` - ); - }); - - test('rejects when both objects and search are passed in', () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - objects: [{ type: 'index-pattern', id: '1' }], - search: 'foo', - }; - - expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Can't specify both \\"search\\" and \\"objects\\" properties when exporting"` - ); - }); - - test('rejects when both objects and references are passed in', () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - objects: [{ type: 'index-pattern', id: '1' }], - hasReference: [{ type: 'index-pattern', id: '1' }], - }; - - expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Can't specify both \\"references\\" and \\"objects\\" properties when exporting"` - ); - }); -}); diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts deleted file mode 100644 index 84b14d0a5f02c..0000000000000 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ /dev/null @@ -1,266 +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 Boom from '@hapi/boom'; -import { createListStream } from '@kbn/utils'; -import { - SavedObjectsClientContract, - SavedObject, - SavedObjectsFindOptionsReference, -} from '../types'; -import { fetchNestedDependencies } from './inject_nested_depdendencies'; -import { sortObjects } from './sort_objects'; - -/** - * Options controlling the export operation. - * @public - */ -export interface SavedObjectsExportOptions { - /** optional array of saved object types. */ - types?: string[]; - /** optional array of references to search object for when exporting by types */ - hasReference?: SavedObjectsFindOptionsReference[]; - /** optional array of objects to export. */ - objects?: Array<{ - /** the saved object id. */ - id: string; - /** the saved object type. */ - type: string; - }>; - /** optional query string to filter exported objects. */ - search?: string; - /** an instance of the SavedObjectsClient. */ - savedObjectsClient: SavedObjectsClientContract; - /** the maximum number of objects to export. */ - exportSizeLimit: number; - /** flag to also include all related saved objects in the export stream. */ - includeReferencesDeep?: boolean; - /** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */ - excludeExportDetails?: boolean; - /** optional namespace to override the namespace used by the savedObjectsClient. */ - namespace?: string; -} - -interface SavedObjectsFetchByTypeOptions { - /** array of saved object types. */ - types: string[]; - /** optional array of references to search object for when exporting by types */ - hasReference?: SavedObjectsFindOptionsReference[]; - /** optional query string to filter exported objects. */ - search?: string; - /** an instance of the SavedObjectsClient. */ - savedObjectsClient: SavedObjectsClientContract; - /** the maximum number of objects to export. */ - exportSizeLimit: number; - /** optional namespace to override the namespace used by the savedObjectsClient. */ - namespace?: string; -} - -interface SavedObjectsFetchByObjectOptions { - /** optional array of objects to export. */ - objects: Array<{ - /** the saved object id. */ - id: string; - /** the saved object type. */ - type: string; - }>; - /** an instance of the SavedObjectsClient. */ - savedObjectsClient: SavedObjectsClientContract; - /** the maximum number of objects to export. */ - exportSizeLimit: number; - /** optional namespace to override the namespace used by the savedObjectsClient. */ - namespace?: string; -} - -const isFetchByTypeOptions = ( - options: SavedObjectsFetchByTypeOptions | SavedObjectsFetchByObjectOptions -): options is SavedObjectsFetchByTypeOptions => { - return Boolean((options as SavedObjectsFetchByTypeOptions).types); -}; - -/** - * Structure of the export result details entry - * @public - */ -export interface SavedObjectsExportResultDetails { - /** number of successfully exported objects */ - exportedCount: number; - /** number of missing references */ - missingRefCount: number; - /** missing references details */ - missingReferences: Array<{ - /** the missing reference id. */ - id: string; - /** the missing reference type. */ - type: string; - }>; -} - -async function fetchByType({ - types, - namespace, - exportSizeLimit, - hasReference, - search, - savedObjectsClient, -}: SavedObjectsFetchByTypeOptions) { - const findResponse = await savedObjectsClient.find({ - type: types, - hasReference, - hasReferenceOperator: hasReference ? 'OR' : undefined, - search, - perPage: exportSizeLimit, - namespaces: namespace ? [namespace] : undefined, - }); - if (findResponse.total > exportSizeLimit) { - throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); - } - - // sorts server-side by _id, since it's only available in fielddata - return ( - findResponse.saved_objects - // exclude the find-specific `score` property from the exported objects - .map(({ score, ...obj }) => obj) - .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1)) - ); -} - -async function fetchByObjects({ - objects, - exportSizeLimit, - namespace, - savedObjectsClient, -}: SavedObjectsFetchByObjectOptions) { - if (objects.length > exportSizeLimit) { - throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); - } - const bulkGetResult = await savedObjectsClient.bulkGet(objects, { namespace }); - const erroredObjects = bulkGetResult.saved_objects.filter((obj) => !!obj.error); - if (erroredObjects.length) { - const err = Boom.badRequest(); - err.output.payload.attributes = { - objects: erroredObjects, - }; - throw err; - } - return bulkGetResult.saved_objects; -} - -const validateOptions = ({ - objects, - search, - hasReference, - exportSizeLimit, - namespace, - savedObjectsClient, - types, -}: SavedObjectsExportOptions): - | SavedObjectsFetchByTypeOptions - | SavedObjectsFetchByObjectOptions => { - if ((types?.length ?? 0) > 0 && (objects?.length ?? 0) > 0) { - throw Boom.badRequest(`Can't specify both "types" and "objects" properties when exporting`); - } - if (objects && objects.length > 0) { - if (objects.length > exportSizeLimit) { - throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); - } - if (typeof search === 'string') { - throw Boom.badRequest(`Can't specify both "search" and "objects" properties when exporting`); - } - if (hasReference && hasReference.length) { - throw Boom.badRequest( - `Can't specify both "references" and "objects" properties when exporting` - ); - } - return { - objects, - exportSizeLimit, - savedObjectsClient, - namespace, - } as SavedObjectsFetchByObjectOptions; - } else if (types && types.length > 0) { - return { - types, - hasReference, - search, - exportSizeLimit, - savedObjectsClient, - namespace, - } as SavedObjectsFetchByTypeOptions; - } else { - throw Boom.badRequest('Either `type` or `objects` are required.'); - } -}; - -/** - * Generates sorted saved object stream to be used for export. - * See the {@link SavedObjectsExportOptions | options} for more detailed information. - * - * @public - */ -export async function exportSavedObjectsToStream({ - types, - hasReference, - objects, - search, - savedObjectsClient, - exportSizeLimit, - includeReferencesDeep = false, - excludeExportDetails = false, - namespace, -}: SavedObjectsExportOptions) { - const fetchOptions = validateOptions({ - savedObjectsClient, - namespace, - exportSizeLimit, - hasReference, - search, - objects, - excludeExportDetails, - includeReferencesDeep, - types, - }); - - const rootObjects = isFetchByTypeOptions(fetchOptions) - ? await fetchByType(fetchOptions) - : await fetchByObjects(fetchOptions); - - let exportedObjects: Array> = []; - let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; - - if (includeReferencesDeep) { - const fetchResult = await fetchNestedDependencies(rootObjects, savedObjectsClient, namespace); - exportedObjects = sortObjects(fetchResult.objects); - missingReferences = fetchResult.missingRefs; - } else { - exportedObjects = sortObjects(rootObjects); - } - - // redact attributes that should not be exported - const redactedObjects = exportedObjects.map>( - ({ namespaces, ...object }) => object - ); - - const exportDetails: SavedObjectsExportResultDetails = { - exportedCount: exportedObjects.length, - missingRefCount: missingReferences.length, - missingReferences, - }; - return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); -} diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index 37824cceb18cb..5166f20b3d1c1 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -18,7 +18,10 @@ */ export { - exportSavedObjectsToStream, - SavedObjectsExportOptions, + SavedObjectsExportByObjectOptions, + SavedObjectExportBaseOptions, + SavedObjectsExportByTypeOptions, SavedObjectsExportResultDetails, -} from './get_sorted_objects_for_export'; +} from './types'; +export { ISavedObjectsExporter, SavedObjectsExporter } from './saved_objects_exporter'; +export { SavedObjectsExportError } from './errors'; diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.mock.ts b/src/core/server/saved_objects/export/saved_objects_exporter.mock.ts new file mode 100644 index 0000000000000..71f08a17e3251 --- /dev/null +++ b/src/core/server/saved_objects/export/saved_objects_exporter.mock.ts @@ -0,0 +1,33 @@ +/* + * 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 { ISavedObjectsExporter } from './saved_objects_exporter'; + +const createExporterMock = () => { + const mock: jest.Mocked = { + exportByObjects: jest.fn(), + exportByTypes: jest.fn(), + }; + + return mock; +}; + +export const savedObjectsExporterMock = { + create: createExporterMock, +}; diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts new file mode 100644 index 0000000000000..b382a36a35ef7 --- /dev/null +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -0,0 +1,936 @@ +/* + * 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 { SavedObjectsExporter } from './saved_objects_exporter'; +import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; +import { Readable } from 'stream'; +import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; + +async function readStreamToCompletion(stream: Readable) { + return createPromiseFromStreams([stream, createConcatStream([])]); +} + +const exportSizeLimit = 500; + +describe('getSortedObjectsForExport()', () => { + let savedObjectsClient: ReturnType; + let exporter: SavedObjectsExporter; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit }); + }); + + describe('#exportByTypes', () => { + test('exports selected types and sorts them', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('omits the `namespaces` property from the export', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + namespaces: ['foo', 'bar'], + score: 0, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + namespaces: ['foo', 'bar'], + score: 0, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('exclude export details if option is specified', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + excludeExportDetails: true, + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); + }); + + test('exports selected types with search string when present', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + search: 'foo', + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 500, + "search": "foo", + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('exports selected types with references when present', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + hasReference: [ + { + id: '1', + type: 'index-pattern', + }, + ], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 1, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + "hasReferenceOperator": "OR", + "namespaces": undefined, + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('exports from the provided namespace when present', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + namespace: 'foo', + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": Array [ + "foo", + ], + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('export selected types throws error when exceeding exportSizeLimit', async () => { + exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1 }); + + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + type: 'index-pattern', + name: 'name', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + await expect( + exporter.exportByTypes({ + types: ['index-pattern', 'search'], + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`); + }); + + test('sorts objects within type', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 3, + per_page: 10000, + page: 1, + saved_objects: [ + { + id: '3', + type: 'index-pattern', + attributes: { + name: 'baz', + }, + score: 1, + references: [], + }, + { + id: '1', + type: 'index-pattern', + attributes: { + name: 'foo', + }, + score: 1, + references: [], + }, + { + id: '2', + type: 'index-pattern', + attributes: { + name: 'bar', + }, + score: 1, + references: [], + }, + ], + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern'], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object { + "name": "foo", + }, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "bar", + }, + "id": "2", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "baz", + }, + "id": "3", + "references": Array [], + "type": "index-pattern", + }, + Object { + "exportedCount": 3, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + }); + }); + + describe('#exportByObjects', () => { + test('exports selected objects and sorts them', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + id: '1', + name: 'name', + type: 'index-pattern', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + ], + }); + const exportStream = await exporter.exportByObjects({ + objects: [ + { + type: 'index-pattern', + id: '1', + }, + { + type: 'search', + id: '2', + }, + ], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + Object { + "id": "2", + "type": "search", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('throws when `bulkGet` returns any errored object', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'search', + attributes: {}, + references: [], + }, + { + id: '2', + type: 'index-pattern', + error: { + error: 'NotFound', + message: 'NotFound', + statusCode: 404, + }, + attributes: {}, + references: [], + }, + ], + }); + await expect( + exporter.exportByObjects({ + objects: [ + { + type: 'index-pattern', + id: '1', + }, + { + type: 'search', + id: '2', + }, + ], + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error fetching objects to export"`); + }); + + test('export selected objects throws error when exceeding exportSizeLimit', async () => { + exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1 }); + + const exportOpts = { + objects: [ + { + type: 'index-pattern', + id: '1', + }, + { + type: 'search', + id: '2', + }, + ], + }; + await expect(exporter.exportByObjects(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't export more than 1 objects"` + ); + }); + + test('modifies return results to redact `namespaces` attribute', async () => { + const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), + createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), + createSavedObject({ type: 'other', id: '3' }), + ], + }); + const exportStream = await exporter.exportByObjects({ + objects: [ + { type: 'multi', id: '1' }, + { type: 'multi', id: '2' }, + { type: 'other', id: '3' }, + ], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toEqual([ + createSavedObject({ type: 'multi', id: '1' }), + createSavedObject({ type: 'multi', id: '2' }), + createSavedObject({ type: 'other', id: '3' }), + expect.objectContaining({ exportedCount: 3 }), + ]); + }); + + test('includes nested dependencies when passed in', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + type: 'index-pattern', + name: 'name', + id: '1', + }, + ], + }, + ], + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + ], + }); + const exportStream = await exporter.exportByObjects({ + objects: [ + { + type: 'search', + id: '2', + }, + ], + includeReferencesDeep: true, + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "2", + "type": "search", + }, + ], + Object { + "namespace": undefined, + }, + ], + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + }); +}); diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts new file mode 100644 index 0000000000000..94b21dda56be1 --- /dev/null +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -0,0 +1,162 @@ +/* + * 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 { createListStream } from '@kbn/utils'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { SavedObject, SavedObjectsClientContract } from '../types'; +import { fetchNestedDependencies } from './fetch_nested_dependencies'; +import { sortObjects } from './sort_objects'; +import { + SavedObjectsExportResultDetails, + SavedObjectExportBaseOptions, + SavedObjectsExportByObjectOptions, + SavedObjectsExportByTypeOptions, +} from './types'; +import { SavedObjectsExportError } from './errors'; + +/** + * @public + */ +export type ISavedObjectsExporter = PublicMethodsOf; + +/** + * @public + */ +export class SavedObjectsExporter { + readonly #savedObjectsClient: SavedObjectsClientContract; + readonly #exportSizeLimit: number; + + constructor({ + savedObjectsClient, + exportSizeLimit, + }: { + savedObjectsClient: SavedObjectsClientContract; + exportSizeLimit: number; + }) { + this.#savedObjectsClient = savedObjectsClient; + this.#exportSizeLimit = exportSizeLimit; + } + + /** + * Generates an export stream for given types. + * + * See the {@link SavedObjectsExportByTypeOptions | options} for more detailed information. + * + * @throws SavedObjectsExportError + */ + public async exportByTypes(options: SavedObjectsExportByTypeOptions) { + const objects = await this.fetchByTypes(options); + return this.processObjects(objects, { + includeReferencesDeep: options.includeReferencesDeep, + excludeExportDetails: options.excludeExportDetails, + namespace: options.namespace, + }); + } + + /** + * Generates an export stream for given object references. + * + * See the {@link SavedObjectsExportByObjectOptions | options} for more detailed information. + * + * @throws SavedObjectsExportError + */ + public async exportByObjects(options: SavedObjectsExportByObjectOptions) { + if (options.objects.length > this.#exportSizeLimit) { + throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit); + } + const objects = await this.fetchByObjects(options); + return this.processObjects(objects, { + includeReferencesDeep: options.includeReferencesDeep, + excludeExportDetails: options.excludeExportDetails, + namespace: options.namespace, + }); + } + + private async processObjects( + savedObjects: SavedObject[], + { + excludeExportDetails = false, + includeReferencesDeep = false, + namespace, + }: SavedObjectExportBaseOptions + ) { + let exportedObjects: Array>; + let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; + + if (includeReferencesDeep) { + const fetchResult = await fetchNestedDependencies( + savedObjects, + this.#savedObjectsClient, + namespace + ); + exportedObjects = sortObjects(fetchResult.objects); + missingReferences = fetchResult.missingRefs; + } else { + exportedObjects = sortObjects(savedObjects); + } + + // redact attributes that should not be exported + const redactedObjects = exportedObjects.map>( + ({ namespaces, ...object }) => object + ); + + const exportDetails: SavedObjectsExportResultDetails = { + exportedCount: exportedObjects.length, + missingRefCount: missingReferences.length, + missingReferences, + }; + return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); + } + + private async fetchByObjects({ objects, namespace }: SavedObjectsExportByObjectOptions) { + const bulkGetResult = await this.#savedObjectsClient.bulkGet(objects, { namespace }); + const erroredObjects = bulkGetResult.saved_objects.filter((obj) => !!obj.error); + if (erroredObjects.length) { + throw SavedObjectsExportError.objectFetchError(erroredObjects); + } + return bulkGetResult.saved_objects; + } + + private async fetchByTypes({ + types, + namespace, + hasReference, + search, + }: SavedObjectsExportByTypeOptions) { + const findResponse = await this.#savedObjectsClient.find({ + type: types, + hasReference, + hasReferenceOperator: hasReference ? 'OR' : undefined, + search, + perPage: this.#exportSizeLimit, + namespaces: namespace ? [namespace] : undefined, + }); + if (findResponse.total > this.#exportSizeLimit) { + throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit); + } + + // sorts server-side by _id, since it's only available in fielddata + return ( + findResponse.saved_objects + // exclude the find-specific `score` property from the exported objects + .map(({ score, ...obj }) => obj) + .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1)) + ); + } +} diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts new file mode 100644 index 0000000000000..0ddcdc361c896 --- /dev/null +++ b/src/core/server/saved_objects/export/types.ts @@ -0,0 +1,77 @@ +/* + * 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 { SavedObjectsFindOptionsReference } from '../types'; + +/** @public */ +export interface SavedObjectExportBaseOptions { + /** flag to also include all related saved objects in the export stream. */ + includeReferencesDeep?: boolean; + /** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */ + excludeExportDetails?: boolean; + /** optional namespace to override the namespace used by the savedObjectsClient. */ + namespace?: string; +} + +/** + * Options for the {@link SavedObjectsExporter.exportByTypes | export by type API} + * + * @public + */ +export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOptions { + /** array of saved object types. */ + types: string[]; + /** optional array of references to search object for. */ + hasReference?: SavedObjectsFindOptionsReference[]; + /** optional query string to filter exported objects. */ + search?: string; +} + +/** + * Options for the {@link SavedObjectsExporter.exportByObjects | export by objects API} + * + * @public + */ +export interface SavedObjectsExportByObjectOptions extends SavedObjectExportBaseOptions { + /** optional array of objects to export. */ + objects: Array<{ + /** the saved object id. */ + id: string; + /** the saved object type. */ + type: string; + }>; +} + +/** + * Structure of the export result details entry + * @public + */ +export interface SavedObjectsExportResultDetails { + /** number of successfully exported objects */ + exportedCount: number; + /** number of missing references */ + missingRefCount: number; + /** missing references details */ + missingReferences: Array<{ + /** the missing reference id. */ + id: string; + /** the missing reference type. */ + type: string; + }>; +} diff --git a/src/core/server/saved_objects/import/errors.ts b/src/core/server/saved_objects/import/errors.ts new file mode 100644 index 0000000000000..eab39fa848523 --- /dev/null +++ b/src/core/server/saved_objects/import/errors.ts @@ -0,0 +1,75 @@ +/* + * 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 { SavedObject } from '../../../types'; + +/** + * @public + */ +export class SavedObjectsImportError extends Error { + private constructor( + public readonly type: string, + message: string, + public readonly attributes?: Record + ) { + super(message); + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, SavedObjectsImportError.prototype); + } + + static importSizeExceeded(limit: number) { + return new SavedObjectsImportError( + 'import-size-exceeded', + `Can't import more than ${limit} objects` + ); + } + + static nonUniqueImportObjects(nonUniqueEntries: string[]) { + return new SavedObjectsImportError( + 'non-unique-entries', + `Non-unique import objects detected: [${nonUniqueEntries.join()}]` + ); + } + + static nonUniqueRetryObjects(nonUniqueRetryObjects: string[]) { + return new SavedObjectsImportError( + 'non-unique-retry-objects', + `Non-unique retry objects: [${nonUniqueRetryObjects.join()}]` + ); + } + + static nonUniqueRetryDestinations(nonUniqueRetryDestinations: string[]) { + return new SavedObjectsImportError( + 'non-unique-retry-destination', + `Non-unique retry destinations: [${nonUniqueRetryDestinations.join()}]` + ); + } + + static referencesFetchError(objects: SavedObject[]) { + return new SavedObjectsImportError( + 'references-fetch-error', + 'Error fetching references for imported objects', + { + objects, + } + ); + } +} diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index 77f49e336a7b9..d9f6ffc280078 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -23,26 +23,28 @@ import { SavedObjectsClientContract, SavedObjectsType, SavedObject, - SavedObjectsImportError, + SavedObjectsImportFailure, } from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsImportOptions, ISavedObjectTypeRegistry } from '..'; +import { ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; -import { importSavedObjectsFromStream } from './import_saved_objects'; - -import { collectSavedObjects } from './collect_saved_objects'; -import { regenerateIds } from './regenerate_ids'; -import { validateReferences } from './validate_references'; -import { checkConflicts } from './check_conflicts'; -import { checkOriginConflicts } from './check_origin_conflicts'; -import { createSavedObjects } from './create_saved_objects'; - -jest.mock('./collect_saved_objects'); -jest.mock('./regenerate_ids'); -jest.mock('./validate_references'); -jest.mock('./check_conflicts'); -jest.mock('./check_origin_conflicts'); -jest.mock('./create_saved_objects'); +import { importSavedObjectsFromStream, ImportSavedObjectsOptions } from './import_saved_objects'; + +import { + collectSavedObjects, + regenerateIds, + validateReferences, + checkConflicts, + checkOriginConflicts, + createSavedObjects, +} from './lib'; + +jest.mock('./lib/collect_saved_objects'); +jest.mock('./lib/regenerate_ids'); +jest.mock('./lib/validate_references'); +jest.mock('./lib/check_conflicts'); +jest.mock('./lib/check_origin_conflicts'); +jest.mock('./lib/create_saved_objects'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => fn as jest.MockedFunction<(...args: Parameters) => U>; @@ -79,17 +81,18 @@ describe('#importSavedObjectsFromStream', () => { let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const setupOptions = (createNewCopies: boolean = false): SavedObjectsImportOptions => { + const setupOptions = ( + createNewCopies: boolean = false, + getTypeImpl: (name: string) => any = (type: string) => + ({ + // other attributes aren't needed for the purposes of injecting metadata + management: { icon: `${type}-icon` }, + } as any) + ): ImportSavedObjectsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - typeRegistry.getType.mockImplementation( - (type: string) => - ({ - // other attributes aren't needed for the purposes of injecting metadata - management: { icon: `${type}-icon` }, - } as any) - ); + typeRegistry.getType.mockImplementation(getTypeImpl); return { readStream, objectLimit, @@ -100,17 +103,20 @@ describe('#importSavedObjectsFromStream', () => { createNewCopies, }; }; - const createObject = (): SavedObject<{ + const createObject = ({ + type = 'foo-type', + title = 'some-title', + }: { type?: string; title?: string } = {}): SavedObject<{ title: string; }> => { return { - type: 'foo-type', + type, id: uuidv4(), references: [], - attributes: { title: 'some-title' }, + attributes: { title }, }; }; - const createError = (): SavedObjectsImportError => { + const createError = (): SavedObjectsImportFailure => { const title = 'some-title'; return { type: 'foo-type', @@ -419,6 +425,51 @@ describe('#importSavedObjectsFromStream', () => { }); }); + test('uses `type.management.getTitle` to resolve the titles', async () => { + const obj1 = createObject({ type: 'foo' }); + const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + + const options = setupOptions(false, (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } + return { + management: { icon: `${type}-icon` }, + }; + }); + + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + pendingOverwrites: new Set(), + }); + getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [obj1, obj2] }); + + const result = await importSavedObjectsFromStream(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { + type: obj1.type, + id: obj1.id, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + }, + { + type: obj2.type, + id: obj2.id, + meta: { title: 'bar-title', icon: `${obj2.type}-icon` }, + }, + ]; + + expect(result).toEqual({ + success: true, + successCount: 2, + successResults, + }); + }); + test('accumulates multiple errors', async () => { const options = setupOptions(); const errors = [createError(), createError(), createError(), createError(), createError()]; diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index 4530c7ff427da..b0debc5b19ef5 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -17,17 +17,38 @@ * under the License. */ -import { collectSavedObjects } from './collect_saved_objects'; +import { Readable } from 'stream'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsClientContract } from '../types'; +import { SavedObjectsImportFailure, SavedObjectsImportResponse } from './types'; import { - SavedObjectsImportError, - SavedObjectsImportResponse, - SavedObjectsImportOptions, -} from './types'; -import { validateReferences } from './validate_references'; -import { checkOriginConflicts } from './check_origin_conflicts'; -import { createSavedObjects } from './create_saved_objects'; -import { checkConflicts } from './check_conflicts'; -import { regenerateIds } from './regenerate_ids'; + validateReferences, + checkOriginConflicts, + createSavedObjects, + checkConflicts, + regenerateIds, + collectSavedObjects, +} from './lib'; + +/** + * Options to control the import operation. + */ +export interface ImportSavedObjectsOptions { + /** The stream of {@link SavedObject | saved objects} to import */ + readStream: Readable; + /** The maximum number of object to import */ + objectLimit: number; + /** If true, will override existing object if present. Note: this has no effect when used with the `createNewCopies` option. */ + overwrite: boolean; + /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ + savedObjectsClient: SavedObjectsClientContract; + /** The registry of all known saved object types */ + typeRegistry: ISavedObjectTypeRegistry; + /** if specified, will import in given namespace, else will import as global object */ + namespace?: string; + /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ + createNewCopies: boolean; +} /** * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more @@ -43,8 +64,8 @@ export async function importSavedObjectsFromStream({ savedObjectsClient, typeRegistry, namespace, -}: SavedObjectsImportOptions): Promise { - let errorAccumulator: SavedObjectsImportError[] = []; +}: ImportSavedObjectsOptions): Promise { + let errorAccumulator: SavedObjectsImportFailure[] = []; const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); // Get the objects to import @@ -111,20 +132,23 @@ export async function importSavedObjectsFromStream({ const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; - const successResults = createSavedObjectsResult.createdObjects.map( - ({ type, id, attributes: { title }, destinationId, originId }) => { - const meta = { title, icon: typeRegistry.getType(type)?.management?.icon }; - const attemptedOverwrite = pendingOverwrites.has(`${type}:${id}`); - return { - type, - id, - meta, - ...(attemptedOverwrite && { overwrite: true }), - ...(destinationId && { destinationId }), - ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), - }; - } - ); + const successResults = createSavedObjectsResult.createdObjects.map((createdObject) => { + const { type, id, destinationId, originId } = createdObject; + const getTitle = typeRegistry.getType(type)?.management?.getTitle; + const meta = { + title: getTitle ? getTitle(createdObject) : createdObject.attributes.title, + icon: typeRegistry.getType(type)?.management?.icon, + }; + const attemptedOverwrite = pendingOverwrites.has(`${type}:${id}`); + return { + type, + id, + meta, + ...(attemptedOverwrite && { overwrite: true }), + ...(destinationId && { destinationId }), + ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), + }; + }); const errorResults = errorAccumulator.map((error) => { const icon = typeRegistry.getType(error.type)?.management?.icon; const attemptedOverwrite = pendingOverwrites.has(`${error.type}:${error.id}`); diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index ab69e4fc44197..d9300f65b1935 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -17,12 +17,11 @@ * under the License. */ -export { importSavedObjectsFromStream } from './import_saved_objects'; -export { resolveSavedObjectsImportErrors } from './resolve_import_errors'; +export { ISavedObjectsImporter, SavedObjectsImporter } from './saved_objects_importer'; export { SavedObjectsImportResponse, SavedObjectsImportSuccess, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportOptions, SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, @@ -32,3 +31,4 @@ export { SavedObjectsResolveImportErrorsOptions, SavedObjectsImportRetry, } from './types'; +export { SavedObjectsImportError } from './errors'; diff --git a/src/core/server/saved_objects/import/__mocks__/index.ts b/src/core/server/saved_objects/import/lib/__mocks__/index.ts similarity index 100% rename from src/core/server/saved_objects/import/__mocks__/index.ts rename to src/core/server/saved_objects/import/lib/__mocks__/index.ts diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts similarity index 97% rename from src/core/server/saved_objects/import/check_conflicts.test.ts rename to src/core/server/saved_objects/import/lib/check_conflicts.test.ts index 0d58970eee2cc..17b4e22e07ebf 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts @@ -18,10 +18,10 @@ */ import { mockUuidv4 } from './__mocks__'; -import { savedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../../mocks'; import { SavedObjectReference, SavedObjectsImportRetry } from 'kibana/public'; -import { SavedObjectsClientContract, SavedObject } from '../types'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsClientContract, SavedObject } from '../../types'; +import { SavedObjectsErrorHelpers } from '../../service'; import { checkConflicts } from './check_conflicts'; type SavedObjectType = SavedObject<{ title?: string }>; diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/lib/check_conflicts.ts similarity index 97% rename from src/core/server/saved_objects/import/check_conflicts.ts rename to src/core/server/saved_objects/import/lib/check_conflicts.ts index 88ef1bf0e0236..25b86834e4e40 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_conflicts.ts @@ -21,10 +21,10 @@ import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectError, SavedObjectsImportRetry, -} from '../types'; +} from '../../types'; interface CheckConflictsParams { objects: Array>; @@ -47,7 +47,7 @@ export async function checkConflicts({ createNewCopies, }: CheckConflictsParams) { const filteredObjects: Array> = []; - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const importIdMap = new Map(); const pendingOverwrites = new Set(); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts similarity index 98% rename from src/core/server/saved_objects/import/check_origin_conflicts.test.ts rename to src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts index ba5576bd05b73..ff7f843a2a8dc 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts @@ -23,12 +23,12 @@ import { SavedObjectReference, SavedObject, SavedObjectsImportRetry, - SavedObjectsImportError, -} from '../types'; + SavedObjectsImportFailure, +} from '../../types'; import { checkOriginConflicts, getImportIdMapForRetries } from './check_origin_conflicts'; -import { savedObjectsClientMock } from '../../mocks'; -import { typeRegistryMock } from '../saved_objects_type_registry.mock'; -import { ISavedObjectTypeRegistry } from '..'; +import { savedObjectsClientMock } from '../../../mocks'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; type SavedObjectType = SavedObject<{ title?: string }>; type CheckOriginConflictsParams = Parameters[0]; @@ -164,7 +164,7 @@ describe('#checkOriginConflicts', () => { const createAmbiguousConflictError = ( object: SavedObjectType, destinations: SavedObjectType[] - ): SavedObjectsImportError => ({ + ): SavedObjectsImportFailure => ({ type: object.type, id: object.id, title: object.attributes.title, @@ -177,7 +177,7 @@ describe('#checkOriginConflicts', () => { const createConflictError = ( object: SavedObjectType, destinationId?: string - ): SavedObjectsImportError => ({ + ): SavedObjectsImportFailure => ({ type: object.type, id: object.id, title: object.attributes?.title, diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts similarity index 98% rename from src/core/server/saved_objects/import/check_origin_conflicts.ts rename to src/core/server/saved_objects/import/lib/check_origin_conflicts.ts index 433574fbdbf4c..3aa7025f21616 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts @@ -22,10 +22,10 @@ import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, -} from '../types'; -import { ISavedObjectTypeRegistry } from '..'; +} from '../../types'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; interface CheckOriginConflictsParams { objects: Array>; @@ -159,7 +159,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo return acc.set(key, [...value, cur.value.object]); }, new Map>>()); - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const importIdMap = new Map(); const pendingOverwrites = new Set(); checkOriginConflictResults.forEach((result) => { diff --git a/src/core/server/saved_objects/import/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts similarity index 96% rename from src/core/server/saved_objects/import/collect_saved_objects.test.ts rename to src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts index f54130be326ad..701c1b9b2aeeb 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts @@ -18,6 +18,7 @@ */ import { Readable, PassThrough } from 'stream'; +import { SavedObjectsImportError } from '../errors'; import { collectSavedObjects } from './collect_saved_objects'; import { createLimitStream } from './create_limit_stream'; import { getNonUniqueEntries } from './get_non_unique_entries'; @@ -112,16 +113,16 @@ describe('collectSavedObjects()', () => { }); describe('results', () => { - test('throws Boom error if any import objects are not unique', async () => { + test('throws import error if any import objects are not unique', async () => { getMockFn(getNonUniqueEntries).mockReturnValue(['type1:id1', 'type2:id2']); const readStream = createReadStream(); expect.assertions(2); try { await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); - } catch ({ isBoom, message }) { - expect(isBoom).toBe(true); - expect(message).toMatchInlineSnapshot( - `"Non-unique import objects detected: [type1:id1,type2:id2]: Bad Request"` + } catch (e) { + expect(e).toBeInstanceOf(SavedObjectsImportError); + expect(e.message).toMatchInlineSnapshot( + `"Non-unique import objects detected: [type1:id1,type2:id2]"` ); } }); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts similarity index 89% rename from src/core/server/saved_objects/import/collect_saved_objects.ts rename to src/core/server/saved_objects/import/lib/collect_saved_objects.ts index 8f09e69f6c727..0494fada87ed9 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts @@ -25,11 +25,11 @@ import { createPromiseFromStreams, } from '@kbn/utils'; -import { SavedObject } from '../types'; -import { createLimitStream } from './create_limit_stream'; -import { SavedObjectsImportError } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportFailure } from '../types'; +import { SavedObjectsImportError } from '../errors'; import { getNonUniqueEntries } from './get_non_unique_entries'; -import { SavedObjectsErrorHelpers } from '..'; +import { createLimitStream } from './create_limit_stream'; interface CollectSavedObjectsOptions { readStream: Readable; @@ -44,7 +44,7 @@ export async function collectSavedObjects({ filter, supportedTypes, }: CollectSavedObjectsOptions) { - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const entries: Array<{ type: string; id: string }> = []; const importIdMap = new Map(); const collectedObjects: Array> = await createPromiseFromStreams([ @@ -79,9 +79,7 @@ export async function collectSavedObjects({ // throw a BadRequest error if we see the same import object type/id more than once const nonUniqueEntries = getNonUniqueEntries(entries); if (nonUniqueEntries.length > 0) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Non-unique import objects detected: [${nonUniqueEntries.join()}]` - ); + throw SavedObjectsImportError.nonUniqueImportObjects(nonUniqueEntries); } return { diff --git a/src/core/server/saved_objects/import/create_limit_stream.test.ts b/src/core/server/saved_objects/import/lib/create_limit_stream.test.ts similarity index 100% rename from src/core/server/saved_objects/import/create_limit_stream.test.ts rename to src/core/server/saved_objects/import/lib/create_limit_stream.test.ts diff --git a/src/core/server/saved_objects/import/create_limit_stream.ts b/src/core/server/saved_objects/import/lib/create_limit_stream.ts similarity index 89% rename from src/core/server/saved_objects/import/create_limit_stream.ts rename to src/core/server/saved_objects/import/lib/create_limit_stream.ts index 709bb3b2d0065..73c2675c0973c 100644 --- a/src/core/server/saved_objects/import/create_limit_stream.ts +++ b/src/core/server/saved_objects/import/lib/create_limit_stream.ts @@ -17,8 +17,8 @@ * under the License. */ -import Boom from '@hapi/boom'; import { Transform } from 'stream'; +import { SavedObjectsImportError } from '../errors'; export function createLimitStream(limit: number) { let counter = 0; @@ -26,7 +26,7 @@ export function createLimitStream(limit: number) { objectMode: true, async transform(obj, enc, done) { if (counter >= limit) { - return done(Boom.badRequest(`Can't import more than ${limit} objects`)); + return done(SavedObjectsImportError.importSizeExceeded(limit)); } counter++; done(undefined, obj); diff --git a/src/core/server/saved_objects/import/create_objects_filter.test.ts b/src/core/server/saved_objects/import/lib/create_objects_filter.test.ts similarity index 100% rename from src/core/server/saved_objects/import/create_objects_filter.test.ts rename to src/core/server/saved_objects/import/lib/create_objects_filter.test.ts diff --git a/src/core/server/saved_objects/import/create_objects_filter.ts b/src/core/server/saved_objects/import/lib/create_objects_filter.ts similarity index 91% rename from src/core/server/saved_objects/import/create_objects_filter.ts rename to src/core/server/saved_objects/import/lib/create_objects_filter.ts index 55b8ab128d753..885b09d1f8adf 100644 --- a/src/core/server/saved_objects/import/create_objects_filter.ts +++ b/src/core/server/saved_objects/import/lib/create_objects_filter.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SavedObject } from '../types'; -import { SavedObjectsImportRetry } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportRetry } from '../types'; export function createObjectsFilter(retries: SavedObjectsImportRetry[]) { const retryKeys = new Set(retries.map((retry) => `${retry.type}:${retry.id}`)); diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts similarity index 95% rename from src/core/server/saved_objects/import/create_saved_objects.test.ts rename to src/core/server/saved_objects/import/lib/create_saved_objects.test.ts index 6c396e58e1a28..8448875e1f7c7 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { savedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../../mocks'; import { createSavedObjects } from './create_saved_objects'; -import { SavedObjectsClientContract, SavedObject, SavedObjectsImportError } from '../types'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsClientContract, SavedObject, SavedObjectsImportFailure } from '../../types'; +import { SavedObjectsErrorHelpers } from '../../service'; import { extractErrors } from './extract_errors'; type CreateSavedObjectsParams = Parameters[0]; @@ -79,7 +79,7 @@ describe('#createSavedObjects', () => { */ const setupParams = (partial: { objects: SavedObject[]; - accumulatedErrors?: SavedObjectsImportError[]; + accumulatedErrors?: SavedObjectsImportFailure[]; namespace?: string; overwrite?: boolean; }): CreateSavedObjectsParams => { @@ -158,7 +158,7 @@ describe('#createSavedObjects', () => { }; test('filters out objects that have errors present', async () => { - const error = { type: obj1.type, id: obj1.id } as SavedObjectsImportError; + const error = { type: obj1.type, id: obj1.id } as SavedObjectsImportFailure; const options = setupParams({ objects: [obj1], accumulatedErrors: [error] }); const createSavedObjectsResult = await createSavedObjects(options); @@ -197,22 +197,26 @@ describe('#createSavedObjects', () => { }; describe('handles accumulated errors as expected', () => { - const resolvableErrors: SavedObjectsImportError[] = [ - { type: 'foo', id: 'foo-id', error: { type: 'conflict' } } as SavedObjectsImportError, + const resolvableErrors: SavedObjectsImportFailure[] = [ + { type: 'foo', id: 'foo-id', error: { type: 'conflict' } } as SavedObjectsImportFailure, { type: 'bar', id: 'bar-id', error: { type: 'ambiguous_conflict' }, - } as SavedObjectsImportError, + } as SavedObjectsImportFailure, { type: 'baz', id: 'baz-id', error: { type: 'missing_references' }, - } as SavedObjectsImportError, + } as SavedObjectsImportFailure, ]; - const unresolvableErrors: SavedObjectsImportError[] = [ - { type: 'qux', id: 'qux-id', error: { type: 'unsupported_type' } } as SavedObjectsImportError, - { type: 'quux', id: 'quux-id', error: { type: 'unknown' } } as SavedObjectsImportError, + const unresolvableErrors: SavedObjectsImportFailure[] = [ + { + type: 'qux', + id: 'qux-id', + error: { type: 'unsupported_type' }, + } as SavedObjectsImportFailure, + { type: 'quux', id: 'quux-id', error: { type: 'unknown' } } as SavedObjectsImportFailure, ]; test('does not call bulkCreate when resolvable errors are present', async () => { diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.ts similarity index 96% rename from src/core/server/saved_objects/import/create_saved_objects.ts rename to src/core/server/saved_objects/import/lib/create_saved_objects.ts index 9930e9c69358a..faddf24983609 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/lib/create_saved_objects.ts @@ -17,13 +17,13 @@ * under the License. */ -import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from '../types'; +import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types'; import { extractErrors } from './extract_errors'; -import { CreatedObject } from './types'; +import { CreatedObject } from '../types'; interface CreateSavedObjectsParams { objects: Array>; - accumulatedErrors: SavedObjectsImportError[]; + accumulatedErrors: SavedObjectsImportFailure[]; savedObjectsClient: SavedObjectsClientContract; importIdMap: Map; namespace?: string; @@ -31,7 +31,7 @@ interface CreateSavedObjectsParams { } interface CreateSavedObjectsResult { createdObjects: Array>; - errors: SavedObjectsImportError[]; + errors: SavedObjectsImportFailure[]; } /** diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/lib/extract_errors.test.ts similarity index 95% rename from src/core/server/saved_objects/import/extract_errors.test.ts rename to src/core/server/saved_objects/import/lib/extract_errors.test.ts index 047c4ae36266f..cafc7a1ff885b 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/lib/extract_errors.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { SavedObject } from '../types'; +import { SavedObject } from '../../types'; import { extractErrors } from './extract_errors'; -import { SavedObjectsErrorHelpers } from '..'; -import { CreatedObject } from './types'; +import { SavedObjectsErrorHelpers } from '../../service'; +import { CreatedObject } from '../types'; describe('extractErrors()', () => { test('returns empty array when no errors exist', () => { diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/lib/extract_errors.ts similarity index 92% rename from src/core/server/saved_objects/import/extract_errors.ts rename to src/core/server/saved_objects/import/lib/extract_errors.ts index 6a7e5d4d9dfa4..6a68adba7f917 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/lib/extract_errors.ts @@ -16,15 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObject } from '../types'; -import { SavedObjectsImportError, CreatedObject } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportFailure, CreatedObject } from '../types'; export function extractErrors( // TODO: define saved object type savedObjectResults: Array>, savedObjectsToImport: Array> ) { - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const originalSavedObjectsMap = new Map>(); for (const savedObject of savedObjectsToImport) { originalSavedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject); diff --git a/src/core/server/saved_objects/import/get_non_unique_entries.test.ts b/src/core/server/saved_objects/import/lib/get_non_unique_entries.test.ts similarity index 100% rename from src/core/server/saved_objects/import/get_non_unique_entries.test.ts rename to src/core/server/saved_objects/import/lib/get_non_unique_entries.test.ts diff --git a/src/core/server/saved_objects/import/get_non_unique_entries.ts b/src/core/server/saved_objects/import/lib/get_non_unique_entries.ts similarity index 100% rename from src/core/server/saved_objects/import/get_non_unique_entries.ts rename to src/core/server/saved_objects/import/lib/get_non_unique_entries.ts diff --git a/src/core/server/saved_objects/import/lib/index.ts b/src/core/server/saved_objects/import/lib/index.ts new file mode 100644 index 0000000000000..2025ca257b616 --- /dev/null +++ b/src/core/server/saved_objects/import/lib/index.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. + */ + +export { checkConflicts } from './check_conflicts'; +export { checkOriginConflicts, getImportIdMapForRetries } from './check_origin_conflicts'; +export { collectSavedObjects } from './collect_saved_objects'; +export { createLimitStream } from './create_limit_stream'; +export { createObjectsFilter } from './create_objects_filter'; +export { createSavedObjects } from './create_saved_objects'; +export { extractErrors } from './extract_errors'; +export { getNonUniqueEntries } from './get_non_unique_entries'; +export { regenerateIds } from './regenerate_ids'; +export { splitOverwrites } from './split_overwrites'; +export { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; +export { validateRetries } from './validate_retries'; diff --git a/src/core/server/saved_objects/import/regenerate_ids.test.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts similarity index 97% rename from src/core/server/saved_objects/import/regenerate_ids.test.ts rename to src/core/server/saved_objects/import/lib/regenerate_ids.test.ts index 1bbc2693e4f49..dbaa3062157c4 100644 --- a/src/core/server/saved_objects/import/regenerate_ids.test.ts +++ b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts @@ -19,7 +19,7 @@ import { mockUuidv4 } from './__mocks__'; import { regenerateIds } from './regenerate_ids'; -import { SavedObject } from '../types'; +import { SavedObject } from '../../types'; describe('#regenerateIds', () => { const objects = ([ diff --git a/src/core/server/saved_objects/import/regenerate_ids.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.ts similarity index 96% rename from src/core/server/saved_objects/import/regenerate_ids.ts rename to src/core/server/saved_objects/import/lib/regenerate_ids.ts index 647386ed16469..6f088f146c3ea 100644 --- a/src/core/server/saved_objects/import/regenerate_ids.ts +++ b/src/core/server/saved_objects/import/lib/regenerate_ids.ts @@ -18,7 +18,7 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { SavedObject } from '../types'; +import { SavedObject } from '../../types'; /** * Takes an array of saved objects and returns an importIdMap of randomly-generated new IDs. diff --git a/src/core/server/saved_objects/import/split_overwrites.test.ts b/src/core/server/saved_objects/import/lib/split_overwrites.test.ts similarity index 100% rename from src/core/server/saved_objects/import/split_overwrites.test.ts rename to src/core/server/saved_objects/import/lib/split_overwrites.test.ts diff --git a/src/core/server/saved_objects/import/split_overwrites.ts b/src/core/server/saved_objects/import/lib/split_overwrites.ts similarity index 93% rename from src/core/server/saved_objects/import/split_overwrites.ts rename to src/core/server/saved_objects/import/lib/split_overwrites.ts index 03ae6b96e7823..9c59a34e23209 100644 --- a/src/core/server/saved_objects/import/split_overwrites.ts +++ b/src/core/server/saved_objects/import/lib/split_overwrites.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SavedObject } from '../types'; -import { SavedObjectsImportRetry } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportRetry } from '../types'; export function splitOverwrites( savedObjects: Array>, diff --git a/src/core/server/saved_objects/import/validate_references.test.ts b/src/core/server/saved_objects/import/lib/validate_references.test.ts similarity index 98% rename from src/core/server/saved_objects/import/validate_references.test.ts rename to src/core/server/saved_objects/import/lib/validate_references.test.ts index 6efd1b28b199d..5611c62610654 100644 --- a/src/core/server/saved_objects/import/validate_references.test.ts +++ b/src/core/server/saved_objects/import/lib/validate_references.test.ts @@ -18,8 +18,8 @@ */ import { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; -import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsErrorHelpers } from '..'; +import { savedObjectsClientMock } from '../../../mocks'; +import { SavedObjectsErrorHelpers } from '../../service'; describe('getNonExistingReferenceAsKeys()', () => { const savedObjectsClient = savedObjectsClientMock.create(); @@ -586,6 +586,8 @@ describe('validateReferences()', () => { ]; await expect( validateReferences(savedObjects, savedObjectsClient) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Bad Request"`); + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error fetching references for imported objects"` + ); }); }); diff --git a/src/core/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/lib/validate_references.ts similarity index 89% rename from src/core/server/saved_objects/import/validate_references.ts rename to src/core/server/saved_objects/import/lib/validate_references.ts index b0686215c00dd..ff5b2e9c8e1d7 100644 --- a/src/core/server/saved_objects/import/validate_references.ts +++ b/src/core/server/saved_objects/import/lib/validate_references.ts @@ -17,14 +17,14 @@ * under the License. */ -import Boom from '@hapi/boom'; -import { SavedObject, SavedObjectsClientContract } from '../types'; -import { SavedObjectsImportError, SavedObjectsImportRetry } from './types'; +import { SavedObject, SavedObjectsClientContract } from '../../types'; +import { SavedObjectsImportFailure, SavedObjectsImportRetry } from '../types'; +import { SavedObjectsImportError } from '../errors'; -const REF_TYPES_TO_VLIDATE = ['index-pattern', 'search']; +const REF_TYPES_TO_VALIDATE = ['index-pattern', 'search']; function filterReferencesToValidate({ type }: { type: string }) { - return REF_TYPES_TO_VLIDATE.includes(type); + return REF_TYPES_TO_VALIDATE.includes(type); } const getObjectsToSkip = (retries: SavedObjectsImportRetry[] = []) => retries.reduce( @@ -70,11 +70,7 @@ export async function getNonExistingReferenceAsKeys( (obj) => obj.error && obj.error.statusCode !== 404 ); if (erroredObjects.length) { - const err = Boom.badRequest(); - err.output.payload.attributes = { - objects: erroredObjects, - }; - throw err; + throw SavedObjectsImportError.referencesFetchError(erroredObjects); } // Cleanup collector @@ -95,7 +91,7 @@ export async function validateReferences( retries?: SavedObjectsImportRetry[] ) { const objectsToSkip = getObjectsToSkip(retries); - const errorMap: { [key: string]: SavedObjectsImportError } = {}; + const errorMap: { [key: string]: SavedObjectsImportFailure } = {}; const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys( savedObjects, savedObjectsClient, diff --git a/src/core/server/saved_objects/import/validate_retries.test.ts b/src/core/server/saved_objects/import/lib/validate_retries.test.ts similarity index 85% rename from src/core/server/saved_objects/import/validate_retries.test.ts rename to src/core/server/saved_objects/import/lib/validate_retries.test.ts index fd3c1e9795f9f..1583296a4a3ab 100644 --- a/src/core/server/saved_objects/import/validate_retries.test.ts +++ b/src/core/server/saved_objects/import/lib/validate_retries.test.ts @@ -18,7 +18,8 @@ */ import { validateRetries } from './validate_retries'; -import { SavedObjectsImportRetry } from '.'; +import { SavedObjectsImportRetry } from '../types'; +import { SavedObjectsImportError } from '../errors'; import { getNonUniqueEntries } from './get_non_unique_entries'; jest.mock('./get_non_unique_entries'); @@ -62,29 +63,29 @@ describe('#validateRetries', () => { }); describe('results', () => { - test('throws Boom error if any retry objects are not unique', () => { + test('throws import error if any retry objects are not unique', () => { mockGetNonUniqueEntries.mockReturnValue(['type1:id1', 'type2:id2']); expect.assertions(2); try { validateRetries([]); - } catch ({ isBoom, message }) { - expect(isBoom).toBe(true); - expect(message).toMatchInlineSnapshot( - `"Non-unique retry objects: [type1:id1,type2:id2]: Bad Request"` + } catch (e) { + expect(e).toBeInstanceOf(SavedObjectsImportError); + expect(e.message).toMatchInlineSnapshot( + `"Non-unique retry objects: [type1:id1,type2:id2]"` ); } }); - test('throws Boom error if any retry destinations are not unique', () => { + test('throws import error if any retry destinations are not unique', () => { mockGetNonUniqueEntries.mockReturnValueOnce([]); mockGetNonUniqueEntries.mockReturnValue(['type1:id1', 'type2:id2']); expect.assertions(2); try { validateRetries([]); - } catch ({ isBoom, message }) { - expect(isBoom).toBe(true); - expect(message).toMatchInlineSnapshot( - `"Non-unique retry destinations: [type1:id1,type2:id2]: Bad Request"` + } catch (e) { + expect(e).toBeInstanceOf(SavedObjectsImportError); + expect(e.message).toMatchInlineSnapshot( + `"Non-unique retry destinations: [type1:id1,type2:id2]"` ); } }); diff --git a/src/core/server/saved_objects/import/validate_retries.ts b/src/core/server/saved_objects/import/lib/validate_retries.ts similarity index 78% rename from src/core/server/saved_objects/import/validate_retries.ts rename to src/core/server/saved_objects/import/lib/validate_retries.ts index f625436edb636..d18f6062b7715 100644 --- a/src/core/server/saved_objects/import/validate_retries.ts +++ b/src/core/server/saved_objects/import/lib/validate_retries.ts @@ -17,16 +17,14 @@ * under the License. */ -import { SavedObjectsImportRetry } from './types'; +import { SavedObjectsImportRetry } from '../types'; import { getNonUniqueEntries } from './get_non_unique_entries'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsImportError } from '../errors'; export const validateRetries = (retries: SavedObjectsImportRetry[]) => { const nonUniqueRetryObjects = getNonUniqueEntries(retries); if (nonUniqueRetryObjects.length > 0) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Non-unique retry objects: [${nonUniqueRetryObjects.join()}]` - ); + throw SavedObjectsImportError.nonUniqueRetryObjects(nonUniqueRetryObjects); } const destinationEntries = retries @@ -34,8 +32,6 @@ export const validateRetries = (retries: SavedObjectsImportRetry[]) => { .map(({ type, destinationId }) => ({ type, id: destinationId! })); const nonUniqueRetryDestinations = getNonUniqueEntries(destinationEntries); if (nonUniqueRetryDestinations.length > 0) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Non-unique retry destinations: [${nonUniqueRetryDestinations.join()}]` - ); + throw SavedObjectsImportError.nonUniqueRetryDestinations(nonUniqueRetryDestinations); } }; diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index 51a48dc511e2a..079bc14342927 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -23,34 +23,39 @@ import { SavedObjectsClientContract, SavedObjectsType, SavedObject, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectReference, } from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsResolveImportErrorsOptions, ISavedObjectTypeRegistry } from '..'; +import { ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; -import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; - -import { validateRetries } from './validate_retries'; -import { collectSavedObjects } from './collect_saved_objects'; -import { regenerateIds } from './regenerate_ids'; -import { validateReferences } from './validate_references'; -import { checkConflicts } from './check_conflicts'; -import { getImportIdMapForRetries } from './check_origin_conflicts'; -import { splitOverwrites } from './split_overwrites'; -import { createSavedObjects } from './create_saved_objects'; -import { createObjectsFilter } from './create_objects_filter'; - -jest.mock('./validate_retries'); -jest.mock('./create_objects_filter'); -jest.mock('./collect_saved_objects'); -jest.mock('./regenerate_ids'); -jest.mock('./validate_references'); -jest.mock('./check_conflicts'); -jest.mock('./check_origin_conflicts'); -jest.mock('./split_overwrites'); -jest.mock('./create_saved_objects'); +import { + resolveSavedObjectsImportErrors, + ResolveSavedObjectsImportErrorsOptions, +} from './resolve_import_errors'; + +import { + validateRetries, + collectSavedObjects, + regenerateIds, + validateReferences, + checkConflicts, + getImportIdMapForRetries, + splitOverwrites, + createSavedObjects, + createObjectsFilter, +} from './lib'; + +jest.mock('./lib/validate_retries'); +jest.mock('./lib/create_objects_filter'); +jest.mock('./lib/collect_saved_objects'); +jest.mock('./lib/regenerate_ids'); +jest.mock('./lib/validate_references'); +jest.mock('./lib/check_conflicts'); +jest.mock('./lib/check_origin_conflicts'); +jest.mock('./lib/split_overwrites'); +jest.mock('./lib/create_saved_objects'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => fn as jest.MockedFunction<(...args: Parameters) => U>; @@ -89,18 +94,18 @@ describe('#importSavedObjectsFromStream', () => { const setupOptions = ( retries: SavedObjectsImportRetry[] = [], - createNewCopies: boolean = false - ): SavedObjectsResolveImportErrorsOptions => { + createNewCopies: boolean = false, + getTypeImpl: (name: string) => any = (type: string) => + ({ + // other attributes aren't needed for the purposes of injecting metadata + management: { icon: `${type}-icon` }, + } as any) + ): ResolveSavedObjectsImportErrorsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - typeRegistry.getType.mockImplementation( - (type: string) => - ({ - // other attributes aren't needed for the purposes of injecting metadata - management: { icon: `${type}-icon` }, - } as any) - ); + typeRegistry.getType.mockImplementation(getTypeImpl); + return { readStream, objectLimit, @@ -122,18 +127,19 @@ describe('#importSavedObjectsFromStream', () => { return { type: 'foo-type', id, overwrite, replaceReferences }; }; const createObject = ( - references?: SavedObjectReference[] + references?: SavedObjectReference[], + { type = 'foo-type', title = 'some-title' }: { type?: string; title?: string } = {} ): SavedObject<{ title: string; }> => { return { - type: 'foo-type', + type, id: uuidv4(), references: references || [], - attributes: { title: 'some-title' }, + attributes: { title }, }; }; - const createError = (): SavedObjectsImportError => { + const createError = (): SavedObjectsImportFailure => { const title = 'some-title'; return { type: 'foo-type', @@ -267,7 +273,7 @@ describe('#importSavedObjectsFromStream', () => { expect(getImportIdMapForRetries).toHaveBeenCalledWith(getImportIdMapForRetriesParams); }); - test('splits objects to ovewrite from those not to overwrite', async () => { + test('splits objects to overwrite from those not to overwrite', async () => { const retries = [createRetry()]; const options = setupOptions(retries); const collectedObjects = [createObject()]; @@ -491,6 +497,55 @@ describe('#importSavedObjectsFromStream', () => { expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); }); + test('uses `type.management.getTitle` to resolve the titles', async () => { + const obj1 = createObject([], { type: 'foo' }); + const obj2 = createObject([], { type: 'bar', title: 'bar-title' }); + + const options = setupOptions([], false, (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } + return { + management: { icon: `${type}-icon` }, + }; + }); + + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + pendingOverwrites: new Set(), + }); + getMockFn(createSavedObjects) + .mockResolvedValueOnce({ errors: [], createdObjects: [obj1, obj2] }) + .mockResolvedValueOnce({ errors: [], createdObjects: [] }); + + const result = await resolveSavedObjectsImportErrors(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { + type: obj1.type, + id: obj1.id, + overwrite: true, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + }, + { + type: obj2.type, + id: obj2.id, + overwrite: true, + meta: { title: 'bar-title', icon: `${obj2.type}-icon` }, + }, + ]; + + expect(result).toEqual({ + success: true, + successCount: 2, + successResults, + }); + }); + test('accumulates multiple errors', async () => { const options = setupOptions(); const errors = [createError(), createError(), createError(), createError()]; diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 2182d9252cd51..9df338a765cfd 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -16,22 +16,46 @@ * specific language governing permissions and limitations * under the License. */ -import { collectSavedObjects } from './collect_saved_objects'; -import { createObjectsFilter } from './create_objects_filter'; -import { splitOverwrites } from './split_overwrites'; + +import { Readable } from 'stream'; +import { SavedObject, SavedObjectsClientContract, SavedObjectsImportRetry } from '../types'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportResponse, - SavedObjectsResolveImportErrorsOptions, SavedObjectsImportSuccess, } from './types'; -import { regenerateIds } from './regenerate_ids'; -import { validateReferences } from './validate_references'; -import { validateRetries } from './validate_retries'; -import { createSavedObjects } from './create_saved_objects'; -import { getImportIdMapForRetries } from './check_origin_conflicts'; -import { SavedObject } from '../types'; -import { checkConflicts } from './check_conflicts'; +import { + collectSavedObjects, + createObjectsFilter, + splitOverwrites, + regenerateIds, + validateReferences, + validateRetries, + createSavedObjects, + getImportIdMapForRetries, + checkConflicts, +} from './lib'; + +/** + * Options to control the "resolve import" operation. + */ +export interface ResolveSavedObjectsImportErrorsOptions { + /** The stream of {@link SavedObject | saved objects} to resolve errors from */ + readStream: Readable; + /** The maximum number of object to import */ + objectLimit: number; + /** client to use to perform the import operation */ + savedObjectsClient: SavedObjectsClientContract; + /** The registry of all known saved object types */ + typeRegistry: ISavedObjectTypeRegistry; + /** saved object import references to retry */ + retries: SavedObjectsImportRetry[]; + /** if specified, will import in given namespace */ + namespace?: string; + /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ + createNewCopies: boolean; +} /** * Resolve and return saved object import errors. @@ -47,12 +71,12 @@ export async function resolveSavedObjectsImportErrors({ typeRegistry, namespace, createNewCopies, -}: SavedObjectsResolveImportErrorsOptions): Promise { +}: ResolveSavedObjectsImportErrorsOptions): Promise { // throw a BadRequest error if we see invalid retries validateRetries(retries); let successCount = 0; - let errorAccumulator: SavedObjectsImportError[] = []; + let errorAccumulator: SavedObjectsImportFailure[] = []; let importIdMap: Map = new Map(); const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); const filter = createObjectsFilter(retries); @@ -153,8 +177,13 @@ export async function resolveSavedObjectsImportErrors({ successCount += createdObjects.length; successResults = [ ...successResults, - ...createdObjects.map(({ type, id, attributes: { title }, destinationId, originId }) => { - const meta = { title, icon: typeRegistry.getType(type)?.management?.icon }; + ...createdObjects.map((createdObject) => { + const { type, id, destinationId, originId } = createdObject; + const getTitle = typeRegistry.getType(type)?.management?.getTitle; + const meta = { + title: getTitle ? getTitle(createdObject) : createdObject.attributes.title, + icon: typeRegistry.getType(type)?.management?.icon, + }; return { type, id, diff --git a/src/core/server/saved_objects/import/saved_objects_importer.mock.ts b/src/core/server/saved_objects/import/saved_objects_importer.mock.ts new file mode 100644 index 0000000000000..d122a9b7c34e5 --- /dev/null +++ b/src/core/server/saved_objects/import/saved_objects_importer.mock.ts @@ -0,0 +1,33 @@ +/* + * 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 { ISavedObjectsImporter } from './saved_objects_importer'; + +const createImporterMock = () => { + const mock: jest.Mocked = { + import: jest.fn(), + resolveImportErrors: jest.fn(), + }; + + return mock; +}; + +export const savedObjectsImporterMock = { + create: createImporterMock, +}; diff --git a/src/core/server/saved_objects/import/saved_objects_importer.ts b/src/core/server/saved_objects/import/saved_objects_importer.ts new file mode 100644 index 0000000000000..11ba104f47b49 --- /dev/null +++ b/src/core/server/saved_objects/import/saved_objects_importer.ts @@ -0,0 +1,103 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { SavedObjectsClientContract } from '../types'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { importSavedObjectsFromStream } from './import_saved_objects'; +import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; +import { + SavedObjectsImportResponse, + SavedObjectsImportOptions, + SavedObjectsResolveImportErrorsOptions, +} from './types'; + +/** + * @public + */ +export type ISavedObjectsImporter = PublicMethodsOf; + +/** + * @public + */ +export class SavedObjectsImporter { + readonly #savedObjectsClient: SavedObjectsClientContract; + readonly #typeRegistry: ISavedObjectTypeRegistry; + readonly #importSizeLimit: number; + + constructor({ + savedObjectsClient, + typeRegistry, + importSizeLimit, + }: { + savedObjectsClient: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + importSizeLimit: number; + }) { + this.#savedObjectsClient = savedObjectsClient; + this.#typeRegistry = typeRegistry; + this.#importSizeLimit = importSizeLimit; + } + + /** + * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more + * detailed information. + * + * @throws SavedObjectsImportError + */ + import({ + readStream, + createNewCopies, + namespace, + overwrite, + }: SavedObjectsImportOptions): Promise { + return importSavedObjectsFromStream({ + readStream, + createNewCopies, + namespace, + overwrite, + objectLimit: this.#importSizeLimit, + savedObjectsClient: this.#savedObjectsClient, + typeRegistry: this.#typeRegistry, + }); + } + + /** + * Resolve and return saved object import errors. + * See the {@link SavedObjectsResolveImportErrorsOptions | options} for more detailed informations. + * + * @throws SavedObjectsImportError + */ + resolveImportErrors({ + readStream, + createNewCopies, + namespace, + retries, + }: SavedObjectsResolveImportErrorsOptions): Promise { + return resolveSavedObjectsImportErrors({ + readStream, + createNewCopies, + namespace, + retries, + objectLimit: this.#importSizeLimit, + savedObjectsClient: this.#savedObjectsClient, + typeRegistry: this.#typeRegistry, + }); + } +} diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index a242ffdf5b50f..5a1793d39739e 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -18,8 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObjectsClientContract, SavedObject } from '../types'; -import { ISavedObjectTypeRegistry } from '..'; +import { SavedObject } from '../types'; /** * Describes a retry operation for importing a saved object. @@ -98,7 +97,7 @@ export interface SavedObjectsImportMissingReferencesError { * Represents a failure to import. * @public */ -export interface SavedObjectsImportError { +export interface SavedObjectsImportFailure { id: string; type: string; /** @@ -154,7 +153,7 @@ export interface SavedObjectsImportResponse { success: boolean; successCount: number; successResults?: SavedObjectsImportSuccess[]; - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; } /** @@ -164,14 +163,8 @@ export interface SavedObjectsImportResponse { export interface SavedObjectsImportOptions { /** The stream of {@link SavedObject | saved objects} to import */ readStream: Readable; - /** The maximum number of object to import */ - objectLimit: number; /** If true, will override existing object if present. Note: this has no effect when used with the `createNewCopies` option. */ overwrite: boolean; - /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ - savedObjectsClient: SavedObjectsClientContract; - /** The registry of all known saved object types */ - typeRegistry: ISavedObjectTypeRegistry; /** if specified, will import in given namespace, else will import as global object */ namespace?: string; /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ @@ -185,12 +178,6 @@ export interface SavedObjectsImportOptions { export interface SavedObjectsResolveImportErrorsOptions { /** The stream of {@link SavedObject | saved objects} to resolve errors from */ readStream: Readable; - /** The maximum number of object to import */ - objectLimit: number; - /** client to use to perform the import operation */ - savedObjectsClient: SavedObjectsClientContract; - /** The registry of all known saved object types */ - typeRegistry: ISavedObjectTypeRegistry; /** saved object import references to retry */ retries: SavedObjectsImportRetry[]; /** if specified, will import in given namespace */ diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 7a0088094e841..2d9e2f2b247fe 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -19,12 +19,31 @@ export * from './service'; -export * from './import'; +export { + ISavedObjectsImporter, + SavedObjectsImporter, + SavedObjectsImportAmbiguousConflictError, + SavedObjectsImportConflictError, + SavedObjectsImportFailure, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportOptions, + SavedObjectsImportResponse, + SavedObjectsImportRetry, + SavedObjectsImportSuccess, + SavedObjectsImportUnknownError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsResolveImportErrorsOptions, + SavedObjectsImportError, +} from './import'; export { - exportSavedObjectsToStream, - SavedObjectsExportOptions, + SavedObjectsExporter, + ISavedObjectsExporter, + SavedObjectExportBaseOptions, + SavedObjectsExportByTypeOptions, + SavedObjectsExportByObjectOptions, SavedObjectsExportResultDetails, + SavedObjectsExportError, } from './export'; export { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 05432d65c0558..1fd2e7352d8f7 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -935,6 +935,7 @@ describe('migration actions', () => { setTimeout(() => { client.indices.putSettings({ + index: 'yellow_then_green_index', body: { index: { number_of_replicas: 0, diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 8f5c19d927d40..6343e535f4db3 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -24,7 +24,11 @@ import { createPromiseFromStreams, createMapStream, createConcatStream } from '@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; -import { exportSavedObjectsToStream } from '../export'; +import { + SavedObjectsExportByTypeOptions, + SavedObjectsExportByObjectOptions, + SavedObjectsExportError, +} from '../export'; import { validateTypes, validateObjects } from './utils'; interface RouteDependencies { @@ -32,6 +36,103 @@ interface RouteDependencies { coreUsageData: CoreUsageDataSetup; } +type EitherExportOptions = SavedObjectsExportByTypeOptions | SavedObjectsExportByObjectOptions; + +interface ExportRawOptions { + type?: string | string[]; + hasReference?: { id: string; type: string } | Array<{ id: string; type: string }>; + objects?: Array<{ id: string; type: string }>; + search?: string; + includeReferencesDeep: boolean; + excludeExportDetails: boolean; +} + +interface ExportOptions { + types?: string[]; + hasReference?: Array<{ id: string; type: string }>; + objects?: Array<{ id: string; type: string }>; + search?: string; + includeReferencesDeep: boolean; + excludeExportDetails: boolean; +} + +const cleanOptions = ({ + type, + objects, + search, + hasReference, + excludeExportDetails, + includeReferencesDeep, +}: ExportRawOptions): ExportOptions => { + return { + types: typeof type === 'string' ? [type] : type, + search, + objects, + hasReference: hasReference && !Array.isArray(hasReference) ? [hasReference] : hasReference, + excludeExportDetails, + includeReferencesDeep, + }; +}; + +const isExportByTypeOptions = ( + options: EitherExportOptions +): options is SavedObjectsExportByTypeOptions => { + return Boolean((options as SavedObjectsExportByTypeOptions).types); +}; + +const validateOptions = ( + { + types, + objects, + excludeExportDetails, + hasReference, + includeReferencesDeep, + search, + }: ExportOptions, + { exportSizeLimit, supportedTypes }: { exportSizeLimit: number; supportedTypes: string[] } +): EitherExportOptions => { + const hasTypes = (types?.length ?? 0) > 0; + const hasObjects = (objects?.length ?? 0) > 0; + if (!hasTypes && !hasObjects) { + throw new Error('Either `type` or `objects` are required.'); + } + if (hasTypes && hasObjects) { + throw new Error(`Can't specify both "types" and "objects" properties when exporting`); + } + if (hasObjects) { + if (objects!.length > exportSizeLimit) { + throw new Error(`Can't export more than ${exportSizeLimit} objects`); + } + if (typeof search === 'string') { + throw new Error(`Can't specify both "search" and "objects" properties when exporting`); + } + if (hasReference && hasReference.length) { + throw new Error(`Can't specify both "references" and "objects" properties when exporting`); + } + const validationError = validateObjects(objects!, supportedTypes); + if (validationError) { + throw new Error(validationError); + } + return { + objects: objects!, + excludeExportDetails, + includeReferencesDeep, + }; + } else { + const validationError = validateTypes(types!, supportedTypes); + if (validationError) { + throw new Error(validationError); + } + return { + types: types!, + hasReference, + search, + excludeExportDetails, + includeReferencesDeep, + }; + } +}; + export const registerExportRoute = ( router: IRouter, { config, coreUsageData }: RouteDependencies @@ -68,73 +169,60 @@ export const registerExportRoute = ( }, }, router.handleLegacyErrors(async (context, req, res) => { - const savedObjectsClient = context.core.savedObjects.client; - const { - type, - hasReference, - objects, - search, - excludeExportDetails, - includeReferencesDeep, - } = req.body; - const types = typeof type === 'string' ? [type] : type; - - // need to access the registry for type validation, can't use the schema for this + const cleaned = cleanOptions(req.body); const supportedTypes = context.core.savedObjects.typeRegistry .getImportableAndExportableTypes() .map((t) => t.name); - if (types) { - const validationError = validateTypes(types, supportedTypes); - if (validationError) { - return res.badRequest({ - body: { - message: validationError, - }, - }); - } + let options: EitherExportOptions; + try { + options = validateOptions(cleaned, { + exportSizeLimit: maxImportExportSize, + supportedTypes, + }); + } catch (e) { + return res.badRequest({ + body: e, + }); } - if (objects) { - const validationError = validateObjects(objects, supportedTypes); - if (validationError) { + + const exporter = context.core.savedObjects.exporter; + + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient + .incrementSavedObjectsExport({ request: req, types: cleaned.types, supportedTypes }) + .catch(() => {}); + + try { + const exportStream = isExportByTypeOptions(options) + ? await exporter.exportByTypes(options) + : await exporter.exportByObjects(options); + + const docsToExport: string[] = await createPromiseFromStreams([ + exportStream, + createMapStream((obj: unknown) => { + return stringify(obj); + }), + createConcatStream([]), + ]); + + return res.ok({ + body: docsToExport.join('\n'), + headers: { + 'Content-Disposition': `attachment; filename="export.ndjson"`, + 'Content-Type': 'application/ndjson', + }, + }); + } catch (e) { + if (e instanceof SavedObjectsExportError) { return res.badRequest({ body: { - message: validationError, + message: e.message, + attributes: e.attributes, }, }); } + throw e; } - - const usageStatsClient = coreUsageData.getClient(); - usageStatsClient - .incrementSavedObjectsExport({ request: req, types, supportedTypes }) - .catch(() => {}); - - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - types, - hasReference: hasReference && !Array.isArray(hasReference) ? [hasReference] : hasReference, - search, - objects, - exportSizeLimit: maxImportExportSize, - includeReferencesDeep, - excludeExportDetails, - }); - - const docsToExport: string[] = await createPromiseFromStreams([ - exportStream, - createMapStream((obj: unknown) => { - return stringify(obj); - }), - createConcatStream([]), - ]); - - return res.ok({ - body: docsToExport.join('\n'), - headers: { - 'Content-Disposition': `attachment; filename="export.ndjson"`, - 'Content-Type': 'application/ndjson', - }, - }); }) ); }; diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index ebc52c32e2c70..abd0f4335d9c5 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -22,8 +22,8 @@ import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; -import { importSavedObjectsFromStream } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; +import { SavedObjectsImportError } from '../import'; import { createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { @@ -41,7 +41,7 @@ export const registerImportRoute = ( router: IRouter, { config, coreUsageData }: RouteDependencies ) => { - const { maxImportExportSize, maxImportPayloadBytes } = config; + const { maxImportPayloadBytes } = config; router.post( { @@ -95,16 +95,26 @@ export const registerImportRoute = ( }); } - const result = await importSavedObjectsFromStream({ - savedObjectsClient: context.core.savedObjects.client, - typeRegistry: context.core.savedObjects.typeRegistry, - readStream, - objectLimit: maxImportExportSize, - overwrite, - createNewCopies, - }); + const { importer } = context.core.savedObjects; + try { + const result = await importer.import({ + readStream, + overwrite, + createNewCopies, + }); - return res.ok({ body: result }); + return res.ok({ body: result }); + } catch (e) { + if (e instanceof SavedObjectsImportError) { + return res.badRequest({ + body: { + message: e.message, + attributes: e.attributes, + }, + }); + } + throw e; + } }) ); }; diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index d5b1e492e573f..752b02aa3dcc6 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -21,19 +21,18 @@ jest.mock('../../export', () => ({ exportSavedObjectsToStream: jest.fn(), })); -import * as exportMock from '../../export'; import supertest from 'supertest'; import type { UnwrapPromise } from '@kbn/utility-types'; import { createListStream } from '@kbn/utils'; import { CoreUsageStatsClient } from '../../../core_usage_data'; import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; +import { savedObjectsExporterMock } from '../../export/saved_objects_exporter.mock'; import { SavedObjectConfig } from '../../saved_objects_config'; import { registerExportRoute } from '../export'; import { setupServer, createExportableType } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; -const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock; const allowedTypes = ['index-pattern', 'search']; const config = { maxImportPayloadBytes: 26214400, @@ -45,12 +44,14 @@ describe('POST /api/saved_objects/_export', () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; + let exporter: ReturnType; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( allowedTypes.map(createExportableType) ); + exporter = handlerContext.savedObjects.exporter; const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); @@ -87,7 +88,7 @@ describe('POST /api/saved_objects/_export', () => { ], }, ]; - exportSavedObjectsToStream.mockResolvedValueOnce(createListStream(sortedObjects)); + exporter.exportByTypes.mockResolvedValueOnce(createListStream(sortedObjects)); const result = await supertest(httpSetup.server.listener) .post('/api/saved_objects/_export') @@ -107,12 +108,10 @@ describe('POST /api/saved_objects/_export', () => { const objects = (result.text as string).split('\n').map((row) => JSON.parse(row)); expect(objects).toEqual(sortedObjects); - expect(exportSavedObjectsToStream.mock.calls[0][0]).toEqual( + expect(exporter.exportByTypes.mock.calls[0][0]).toEqual( expect.objectContaining({ excludeExportDetails: false, - exportSizeLimit: 10000, includeReferencesDeep: true, - objects: undefined, search: 'my search string', types: ['search'], }) diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index b80deb87725d4..16d07f2a94d3a 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { mockUuidv4 } from '../../import/__mocks__'; +import { mockUuidv4 } from '../../import/lib/__mocks__'; import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerImportRoute } from '../import'; @@ -27,7 +27,7 @@ import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_st import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { SavedObjectConfig } from '../../saved_objects_config'; import { setupServer, createExportableType } from '../test_utils'; -import { SavedObjectsErrorHelpers } from '../..'; +import { SavedObjectsErrorHelpers, SavedObjectsImporter } from '../..'; type SetupServerReturn = UnwrapPromise>; @@ -74,6 +74,15 @@ describe(`POST ${URL}`, () => { savedObjectsClient.find.mockResolvedValue(emptyResponse); savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); + const importer = new SavedObjectsImporter({ + savedObjectsClient, + typeRegistry: handlerContext.savedObjects.typeRegistry, + importSizeLimit: 10000, + }); + handlerContext.savedObjects.importer.import.mockImplementation((options) => + importer.import(options) + ); + const router = httpSetup.createRouter('/internal/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index f135e34231cb6..2207f2c69ec74 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { mockUuidv4 } from '../../import/__mocks__'; +import { mockUuidv4 } from '../../import/lib/__mocks__'; import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerResolveImportErrorsRoute } from '../resolve_import_errors'; @@ -27,6 +27,7 @@ import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_st import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer, createExportableType } from '../test_utils'; import { SavedObjectConfig } from '../../saved_objects_config'; +import { SavedObjectsImporter } from '../..'; type SetupServerReturn = UnwrapPromise>; @@ -79,6 +80,15 @@ describe(`POST ${URL}`, () => { savedObjectsClient = handlerContext.savedObjects.client; savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); + const importer = new SavedObjectsImporter({ + savedObjectsClient, + typeRegistry: handlerContext.savedObjects.typeRegistry, + importSizeLimit: 10000, + }); + handlerContext.savedObjects.importer.resolveImportErrors.mockImplementation((options) => + importer.resolveImportErrors(options) + ); + const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsResolveImportErrors.mockRejectedValue( diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 5db5454b224d7..5df0a862fee2a 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -22,8 +22,8 @@ import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; -import { resolveSavedObjectsImportErrors } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; +import { SavedObjectsImportError } from '../import'; import { createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { @@ -41,7 +41,7 @@ export const registerResolveImportErrorsRoute = ( router: IRouter, { config, coreUsageData }: RouteDependencies ) => { - const { maxImportExportSize, maxImportPayloadBytes } = config; + const { maxImportPayloadBytes } = config; router.post( { @@ -103,16 +103,27 @@ export const registerResolveImportErrorsRoute = ( }); } - const result = await resolveSavedObjectsImportErrors({ - typeRegistry: context.core.savedObjects.typeRegistry, - savedObjectsClient: context.core.savedObjects.client, - readStream, - retries: req.body.retries, - objectLimit: maxImportExportSize, - createNewCopies, - }); + const { importer } = context.core.savedObjects; - return res.ok({ body: result }); + try { + const result = await importer.resolveImportErrors({ + readStream, + retries: req.body.retries, + createNewCopies, + }); + + return res.ok({ body: result }); + } catch (e) { + if (e instanceof SavedObjectsImportError) { + return res.badRequest({ + body: { + message: e.message, + attributes: e.attributes, + }, + }); + } + throw e; + } }) ); }; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 85dbf4b5e8c6a..1a920501541b6 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -30,6 +30,8 @@ import type { import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; +import { savedObjectsExporterMock } from './export/saved_objects_exporter.mock'; +import { savedObjectsImporterMock } from './import/saved_objects_importer.mock'; import { migrationMocks } from './migrations/mocks'; import { ServiceStatusLevels } from '../status'; import { ISavedObjectTypeRegistry } from './saved_objects_type_registry'; @@ -42,6 +44,8 @@ const createStartContractMock = (typeRegistry?: jest.Mocked { setClientFactoryProvider: jest.fn(), addClientWrapper: jest.fn(), registerType: jest.fn(), - getImportExportObjectLimit: jest.fn(), }; - setupContract.getImportExportObjectLimit.mockReturnValue(100); - return setupContract; }; @@ -106,4 +109,6 @@ export const savedObjectsServiceMock = { createStartContract: createStartContractMock, createMigrationContext: migrationMocks.createContext, createTypeRegistryMock: typeRegistryMock.create, + createExporter: savedObjectsExporterMock.create, + createImporter: savedObjectsImporterMock.create, }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index d2e4d8c5cbb2d..c34da35a35531 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -49,6 +49,8 @@ import { import { Logger } from '../logging'; import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; import { SavedObjectsSerializer } from './serialization'; +import { SavedObjectsExporter, ISavedObjectsExporter } from './export'; +import { SavedObjectsImporter, ISavedObjectsImporter } from './import'; import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; @@ -149,11 +151,6 @@ export interface SavedObjectsServiceSetup { * ``` */ registerType: (type: SavedObjectsType) => void; - - /** - * Returns the maximum number of objects allowed for import or export operations. - */ - getImportExportObjectLimit: () => number; } /** @@ -212,6 +209,14 @@ export interface SavedObjectsServiceStart { * Creates a {@link SavedObjectsSerializer | serializer} that is aware of all registered types. */ createSerializer: () => SavedObjectsSerializer; + /** + * Creates an {@link ISavedObjectsExporter | exporter} bound to given client. + */ + createExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; + /** + * Creates an {@link ISavedObjectsImporter | importer} bound to given client. + */ + createImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; /** * Returns the {@link ISavedObjectTypeRegistry | registry} containing all registered * {@link SavedObjectsType | saved object types} @@ -340,7 +345,6 @@ export class SavedObjectsService } this.typeRegistry.registerType(type); }, - getImportExportObjectLimit: () => this.config!.maxImportExportSize, }; } @@ -451,6 +455,17 @@ export class SavedObjectsService createScopedRepository: repositoryFactory.createScopedRepository, createInternalRepository: repositoryFactory.createInternalRepository, createSerializer: () => new SavedObjectsSerializer(this.typeRegistry), + createExporter: (savedObjectsClient) => + new SavedObjectsExporter({ + savedObjectsClient, + exportSizeLimit: this.config!.maxImportExportSize, + }), + createImporter: (savedObjectsClient) => + new SavedObjectsImporter({ + savedObjectsClient, + typeRegistry: this.typeRegistry, + importSizeLimit: this.config!.maxImportExportSize, + }), getTypeRegistry: () => this.typeRegistry, }; } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index b16eeb2aa03a6..c8f8b47949ca5 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -29,7 +29,7 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, } from './import/types'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index cef5f33726ed5..8c284facb442e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -123,6 +123,7 @@ import { PackageInfo } from '@kbn/config'; import { PathConfigType } from '@kbn/utils'; import { PeerCertificate } from 'tls'; import { PingParams } from 'elasticsearch'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; import { Readable } from 'stream'; @@ -310,7 +311,7 @@ export interface CapabilitiesStart { } // @public -export type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities) => Partial | Promise>; +export type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities, useDefaultCapabilities: boolean) => Partial | Promise>; // @alpha export const config: { @@ -907,9 +908,6 @@ export interface Explanation { value: number; } -// @public -export function exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; - // @public export interface FakeRequest { headers: Headers; @@ -1123,9 +1121,6 @@ export interface ImageValidation { }; } -// @public -export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; - // @public @deprecated (undocumented) export interface IndexSettingsDeprecationInfo { // (undocumented) @@ -1157,6 +1152,12 @@ export interface IRouter { // @public export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; +// @public (undocumented) +export type ISavedObjectsExporter = PublicMethodsOf; + +// @public (undocumented) +export type ISavedObjectsImporter = PublicMethodsOf; + // @public export type ISavedObjectsRepository = Pick; @@ -1894,6 +1895,8 @@ export interface RequestHandlerContext { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; + exporter: ISavedObjectsExporter; + importer: ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; @@ -1916,9 +1919,6 @@ export type RequestHandlerContextProvider(handler: RequestHandler) => RequestHandler; -// @public -export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, }: SavedObjectsResolveImportErrorsOptions): Promise; - // @public export type ResponseError = string | Error | { message: string | Error; @@ -2049,6 +2049,13 @@ export interface SavedObjectAttributes { // @public export type SavedObjectAttributeSingle = string | number | boolean | null | undefined | SavedObjectAttributes; +// @public (undocumented) +export interface SavedObjectExportBaseOptions { + excludeExportDetails?: boolean; + includeReferencesDeep?: boolean; + namespace?: string; +} + // @public export interface SavedObjectMigrationContext { log: SavedObjectsMigrationLogger; @@ -2350,19 +2357,43 @@ export class SavedObjectsErrorHelpers { } // @public -export interface SavedObjectsExportOptions { - excludeExportDetails?: boolean; - exportSizeLimit: number; - hasReference?: SavedObjectsFindOptionsReference[]; - includeReferencesDeep?: boolean; - namespace?: string; - objects?: Array<{ +export interface SavedObjectsExportByObjectOptions extends SavedObjectExportBaseOptions { + objects: Array<{ id: string; type: string; }>; - savedObjectsClient: SavedObjectsClientContract; +} + +// @public +export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOptions { + hasReference?: SavedObjectsFindOptionsReference[]; search?: string; - types?: string[]; + types: string[]; +} + +// @public (undocumented) +export class SavedObjectsExporter { + // (undocumented) + #private; + constructor({ savedObjectsClient, exportSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + exportSizeLimit: number; + }); + exportByObjects(options: SavedObjectsExportByObjectOptions): Promise; + exportByTypes(options: SavedObjectsExportByTypeOptions): Promise; + } + +// @public (undocumented) +export class SavedObjectsExportError extends Error { + constructor(type: string, message: string, attributes?: Record | undefined); + // (undocumented) + readonly attributes?: Record | undefined; + // (undocumented) + static exportSizeExceeded(limit: number): SavedObjectsExportError; + // (undocumented) + static objectFetchError(objects: SavedObject[]): SavedObjectsExportError; + // (undocumented) + readonly type: string; } // @public @@ -2452,8 +2483,39 @@ export interface SavedObjectsImportConflictError { type: 'conflict'; } +// @public (undocumented) +export class SavedObjectsImporter { + // (undocumented) + #private; + constructor({ savedObjectsClient, typeRegistry, importSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + importSizeLimit: number; + }); + import({ readStream, createNewCopies, namespace, overwrite, }: SavedObjectsImportOptions): Promise; + resolveImportErrors({ readStream, createNewCopies, namespace, retries, }: SavedObjectsResolveImportErrorsOptions): Promise; +} + +// @public (undocumented) +export class SavedObjectsImportError extends Error { + // (undocumented) + readonly attributes?: Record | undefined; + // (undocumented) + static importSizeExceeded(limit: number): SavedObjectsImportError; + // (undocumented) + static nonUniqueImportObjects(nonUniqueEntries: string[]): SavedObjectsImportError; + // (undocumented) + static nonUniqueRetryDestinations(nonUniqueRetryDestinations: string[]): SavedObjectsImportError; + // (undocumented) + static nonUniqueRetryObjects(nonUniqueRetryObjects: string[]): SavedObjectsImportError; + // (undocumented) + static referencesFetchError(objects: SavedObject[]): SavedObjectsImportError; + // (undocumented) + readonly type: string; +} + // @public -export interface SavedObjectsImportError { +export interface SavedObjectsImportFailure { // (undocumented) error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; // (undocumented) @@ -2485,17 +2547,14 @@ export interface SavedObjectsImportMissingReferencesError { export interface SavedObjectsImportOptions { createNewCopies: boolean; namespace?: string; - objectLimit: number; overwrite: boolean; readStream: Readable; - savedObjectsClient: SavedObjectsClientContract; - typeRegistry: ISavedObjectTypeRegistry; } // @public export interface SavedObjectsImportResponse { // (undocumented) - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; // (undocumented) success: boolean; // (undocumented) @@ -2656,11 +2715,8 @@ export interface SavedObjectsRepositoryFactory { export interface SavedObjectsResolveImportErrorsOptions { createNewCopies: boolean; namespace?: string; - objectLimit: number; readStream: Readable; retries: SavedObjectsImportRetry[]; - savedObjectsClient: SavedObjectsClientContract; - typeRegistry: ISavedObjectTypeRegistry; } // @public @@ -2676,13 +2732,14 @@ export class SavedObjectsSerializer { // @public export interface SavedObjectsServiceSetup { addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; - getImportExportObjectLimit: () => number; registerType: (type: SavedObjectsType) => void; setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void; } // @public export interface SavedObjectsServiceStart { + createExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; + createImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; createInternalRepository: (includedHiddenTypes?: string[]) => ISavedObjectsRepository; createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository; createSerializer: () => SavedObjectsSerializer; diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 1081d5d0d6dbd..4613303808f8e 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -61,6 +61,7 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions */ await run(Tasks.CopySource); await run(Tasks.CopyBinScripts); + await run(Tasks.ReplaceFavicon); await run(Tasks.CreateEmptyDirsAndFiles); await run(Tasks.CreateReadme); await run(Tasks.BuildPackages); diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 710e504e58868..038ccba5ed17e 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -31,6 +31,8 @@ export const CopySource: Task = { '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', + '!src/core/server/core_app/assets/favicons/favicon.distribution.png', + '!src/core/server/core_app/assets/favicons/favicon.distribution.svg', '!src/test_utils/**', '!src/fixtures/**', '!src/cli/repl/**', diff --git a/src/dev/build/tasks/create_archives_task.ts b/src/dev/build/tasks/create_archives_task.ts index a05e383394ecf..2e5b54ccb09d5 100644 --- a/src/dev/build/tasks/create_archives_task.ts +++ b/src/dev/build/tasks/create_archives_task.ts @@ -21,7 +21,7 @@ import Path from 'path'; import Fs from 'fs'; import { promisify } from 'util'; -import { CiStatsReporter, CiStatsMetrics } from '@kbn/dev-utils'; +import { CiStatsMetrics } from '@kbn/dev-utils'; import { mkdirp, compressTar, compressZip, Task } from '../lib'; @@ -99,6 +99,7 @@ export const CreateArchives: Task = { } log.debug('archive metrics:', metrics); - await CiStatsReporter.fromEnv(log).metrics(metrics); + // FLAKY: https://github.com/elastic/kibana/issues/87529 + // await CiStatsReporter.fromEnv(log).metrics(metrics); }, }; diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index ec0de7ca84aad..ca10fcca80498 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -38,6 +38,7 @@ export * from './transpile_babel_task'; export * from './uuid_verification_task'; export * from './verify_env_task'; export * from './write_sha_sums_task'; +export * from './replace_favicon'; // @ts-expect-error this module can't be TS because it ends up pulling x-pack into Kibana export { InstallChromium } from './install_chromium'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 3e440c89b82d8..6822fcddc3ac5 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -169,6 +169,7 @@ kibana_vars=( xpack.fleet.agents.elasticsearch.host xpack.fleet.agents.kibana.host xpack.fleet.agents.tlsCheckDisabled + xpack.fleet.registryUrl xpack.graph.enabled xpack.graph.canEditDrillDownUrls xpack.graph.savePolicy diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 240ec6f4e9326..a849c6bf4992d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -29,7 +29,7 @@ function generator({ imageFlavor }: TemplateContext) { # Default Kibana configuration for docker target server.name: kibana - server.host: "0" + server.host: "0.0.0.0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} `); diff --git a/src/dev/build/tasks/replace_favicon.ts b/src/dev/build/tasks/replace_favicon.ts new file mode 100644 index 0000000000000..bdf5764b0f4e7 --- /dev/null +++ b/src/dev/build/tasks/replace_favicon.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 { copy, Task } from '../lib'; + +export const ReplaceFavicon: Task = { + description: 'Replacing favicons with built version', + + async run(config, log, build) { + await copy( + config.resolveFromRepo('src/core/server/core_app/assets/favicons/favicon.distribution.png'), + build.resolvePath('src/core/server/core_app/assets/favicons/favicon.png') + ); + + await copy( + config.resolveFromRepo('src/core/server/core_app/assets/favicons/favicon.distribution.svg'), + build.resolvePath('src/core/server/core_app/assets/favicons/favicon.svg') + ); + }, +}; diff --git a/src/dev/cli_dev_mode/log.ts b/src/dev/cli_dev_mode/log.ts index f349026ca9cab..3a5d60e65c3f1 100644 --- a/src/dev/cli_dev_mode/log.ts +++ b/src/dev/cli_dev_mode/log.ts @@ -25,7 +25,7 @@ export interface Log { good(label: string, ...args: any[]): void; warn(label: string, ...args: any[]): void; bad(label: string, ...args: any[]): void; - write(label: string, ...args: any[]): void; + write(...args: any[]): void; } export class CliLog implements Log { @@ -58,9 +58,9 @@ export class CliLog implements Log { console.log(Chalk.white.bgRed(` ${label.trim()} `), ...args); } - write(label: string, ...args: any[]) { + write(...args: any[]) { // eslint-disable-next-line no-console - console.log(` ${label.trim()} `, ...args); + console.log(...args); } } @@ -88,10 +88,10 @@ export class TestLog implements Log { }); } - write(label: string, ...args: any[]) { + write(...args: any[]) { this.messages.push({ type: 'write', - args: [label, ...args], + args, }); } } diff --git a/src/dev/cli_dev_mode/optimizer.test.ts b/src/dev/cli_dev_mode/optimizer.test.ts index 8a82012499b33..6017ab2c35d0f 100644 --- a/src/dev/cli_dev_mode/optimizer.test.ts +++ b/src/dev/cli_dev_mode/optimizer.test.ts @@ -191,8 +191,8 @@ it('is ready when optimizer phase is success or issue and logs in familiar forma const lines = await linesPromise; expect(lines).toMatchInlineSnapshot(` Array [ - "np bld log [timestamp] [success][@kbn/optimizer] 0 bundles compiled successfully after 0 sec", - "np bld log [timestamp] [error][@kbn/optimizer] webpack compile errors", + " np bld log [timestamp] [success][@kbn/optimizer] 0 bundles compiled successfully after 0 sec", + " np bld log [timestamp] [error][@kbn/optimizer] webpack compile errors", ] `); }); diff --git a/src/dev/cli_dev_mode/optimizer.ts b/src/dev/cli_dev_mode/optimizer.ts index 9aac414f02b29..f618a0fdbe72f 100644 --- a/src/dev/cli_dev_mode/optimizer.ts +++ b/src/dev/cli_dev_mode/optimizer.ts @@ -105,7 +105,7 @@ export class Optimizer { ToolingLogTextWriter.write( options.writeLogTo ?? process.stdout, - `${dim} log [${time()}] [${level(msg.type)}][${name}] `, + ` ${dim} log [${time()}] [${level(msg.type)}][${name}] `, msg ); return true; diff --git a/src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh b/src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh deleted file mode 100644 index 579276aac990f..0000000000000 --- a/src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -EXTRACT_START_DIR=tmp/extracted_coverage -EXTRACT_END_DIR=target/kibana-coverage -COMBINED_EXRACT_DIR=/${EXTRACT_START_DIR}/${EXTRACT_END_DIR} - - -echo "### Copy mocha reports" -mkdir -p $EXTRACT_END_DIR/mocha-combined -cp -r $COMBINED_EXRACT_DIR/mocha/. $EXTRACT_END_DIR/mocha-combined/ diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 62b81929ae79b..caa1f1a761367 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -40,11 +40,5 @@ for x in jest functional; do node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH done -# Need to override COVERAGE_INGESTION_KIBANA_ROOT since mocha json file has original intake worker path -COVERAGE_SUMMARY_FILE=target/kibana-coverage/mocha-combined/coverage-summary.json -export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana - -node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH - echo "### Ingesting Code Coverage - Complete" echo "" diff --git a/tasks/function_test_groups.js b/src/dev/run_ensure_all_tests_in_ci_group.js similarity index 65% rename from tasks/function_test_groups.js rename to src/dev/run_ensure_all_tests_in_ci_group.js index 0b456dcb0da13..b5d36c405cbbb 100644 --- a/tasks/function_test_groups.js +++ b/src/dev/run_ensure_all_tests_in_ci_group.js @@ -21,32 +21,28 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; import execa from 'execa'; -import grunt from 'grunt'; import { safeLoad } from 'js-yaml'; -const JOBS_YAML = readFileSync(resolve(__dirname, '../.ci/jobs.yml'), 'utf8'); +import { run } from '@kbn/dev-utils'; + +const JOBS_YAML = readFileSync(resolve(__dirname, '../../.ci/jobs.yml'), 'utf8'); const TEST_TAGS = safeLoad(JOBS_YAML) .JOB.filter((id) => id.startsWith('kibana-ciGroup')) .map((id) => id.replace(/^kibana-/, '')); -grunt.registerTask( - 'functionalTests:ensureAllTestsInCiGroup', - 'Check that all of the functional tests are in a CI group', - async function () { - const done = this.async(); - - try { - const result = await execa(process.execPath, [ - 'scripts/functional_test_runner', - ...TEST_TAGS.map((tag) => `--include-tag=${tag}`), - '--config', - 'test/functional/config.js', - '--test-stats', - ]); - const stats = JSON.parse(result.stderr); - - if (stats.excludedTests.length > 0) { - grunt.fail.fatal(` +run(async ({ log }) => { + try { + const result = await execa(process.execPath, [ + 'scripts/functional_test_runner', + ...TEST_TAGS.map((tag) => `--include-tag=${tag}`), + '--config', + 'test/functional/config.js', + '--test-stats', + ]); + const stats = JSON.parse(result.stderr); + + if (stats.excludedTests.length > 0) { + log.error(` ${stats.excludedTests.length} tests are excluded by the ciGroup tags, make sure that all test suites have a "ciGroup{X}" tag and that "tasks/functional_test_groups.js" knows about the tag that you are using. @@ -55,12 +51,11 @@ grunt.registerTask( - ${stats.excludedTests.join('\n - ')} `); - return; - } - - done(); - } catch (error) { - grunt.fail.fatal(error.stack); + process.exitCode = 1; + return; } + } catch (error) { + log.error(error.stack); + process.exitCode = 1; } -); +}); diff --git a/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts b/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts index 30ec0ac8ef52e..cfbef768f6874 100644 --- a/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts +++ b/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Fs from 'fs'; +import JSON5 from 'json5'; import { get } from 'lodash'; import { run, KibanaPlatformPlugin } from '@kbn/dev-utils'; import { getPluginDeps, findPlugins } from './plugin_discovery'; @@ -46,7 +47,8 @@ run( id: pluginId, }); - if (deps.size === 0 && errors.size === 0) { + const allDepsMigrated = [...deps].every((p) => isMigratedToTsProjectRefs(p.directory)); + if (allDepsMigrated && errors.size === 0) { readyToMigrate.add(pluginMap.get(pluginId)!); } } @@ -82,7 +84,7 @@ function isMigratedToTsProjectRefs(dir: string): boolean { try { const path = Path.join(dir, 'tsconfig.json'); const content = Fs.readFileSync(path, { encoding: 'utf8' }); - return get(JSON.parse(content), 'compilerOptions.composite', false); + return get(JSON5.parse(content), 'compilerOptions.composite', false); } catch (e) { return false; } diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index 1a087e2a01fb2..75faf3d8c17a7 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -31,10 +31,6 @@ interface Options { type CircularDepList = Set; const allowedList: CircularDepList = new Set([ - 'src/plugins/charts -> src/plugins/discover', - 'src/plugins/charts -> src/plugins/vis_default_editor', - 'src/plugins/vis_default_editor -> src/plugins/visualizations', - 'src/plugins/vis_default_editor -> src/plugins/visualize', 'src/plugins/visualizations -> src/plugins/visualize', 'x-pack/plugins/actions -> x-pack/plugins/case', 'x-pack/plugins/case -> x-pack/plugins/security_solution', diff --git a/src/dev/run_find_plugins_without_ts_refs.ts b/src/dev/run_find_plugins_without_ts_refs.ts index ad63884671e24..995a22bf3e583 100644 --- a/src/dev/run_find_plugins_without_ts_refs.ts +++ b/src/dev/run_find_plugins_without_ts_refs.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Fs from 'fs'; +import JSON5 from 'json5'; import { get } from 'lodash'; import { run } from '@kbn/dev-utils'; import { getPluginDeps, findPlugins } from './plugin_discovery'; @@ -88,7 +89,7 @@ function isMigratedToTsProjectRefs(dir: string): boolean { try { const path = Path.join(dir, 'tsconfig.json'); const content = Fs.readFileSync(path, { encoding: 'utf8' }); - return get(JSON.parse(content), 'compilerOptions.composite', false); + return get(JSON5.parse(content), 'compilerOptions.composite', false); } catch (e) { return false; } diff --git a/src/plugins/apm_oss/server/tutorial/index_pattern.json b/src/plugins/apm_oss/server/tutorial/index_pattern.json index b9f3b43b67b84..6eb040f2758af 100644 --- a/src/plugins/apm_oss/server/tutorial/index_pattern.json +++ b/src/plugins/apm_oss/server/tutorial/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.build.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reason\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hosts\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.build.original\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reason\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.file.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hosts\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, diff --git a/src/plugins/apm_oss/tsconfig.json b/src/plugins/apm_oss/tsconfig.json new file mode 100644 index 0000000000000..aeb6837c69a99 --- /dev/null +++ b/src/plugins/apm_oss/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/tutorial/index_pattern.json" + ], + "references": [{ "path": "../../core/tsconfig.json" }, { "path": "../home/tsconfig.json" }] +} diff --git a/src/plugins/bfetch/tsconfig.json b/src/plugins/bfetch/tsconfig.json new file mode 100644 index 0000000000000..173ff725d07d0 --- /dev/null +++ b/src/plugins/bfetch/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*", "index.ts"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + ] +} diff --git a/src/plugins/charts/kibana.json b/src/plugins/charts/kibana.json index a6d4dbba7238f..4510a1ea7d065 100644 --- a/src/plugins/charts/kibana.json +++ b/src/plugins/charts/kibana.json @@ -3,6 +3,5 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["expressions"], - "requiredBundles": ["visDefaultEditor"] + "requiredPlugins": ["expressions"] } diff --git a/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts b/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts index 2934d4208d22c..7848cdd3f3140 100644 --- a/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts +++ b/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts @@ -37,15 +37,15 @@ export class MappedColors { private _mapping: any; constructor( - private uiSettings: CoreSetup['uiSettings'], + private uiSettings?: CoreSetup['uiSettings'], private colorPaletteFn: (num: number) => string[] = createColorPalette ) { this._oldMap = {}; this._mapping = {}; } - private getConfigColorMapping() { - return _.mapValues(this.uiSettings.get(COLOR_MAPPING_SETTING), standardizeColor); + private getConfigColorMapping(): Record { + return _.mapValues(this.uiSettings?.get(COLOR_MAPPING_SETTING) || {}, standardizeColor); } public get oldMap(): any { diff --git a/src/plugins/charts/public/services/palettes/palettes.test.tsx b/src/plugins/charts/public/services/palettes/palettes.test.tsx index 5d9337f1ee683..7356f13fddf9f 100644 --- a/src/plugins/charts/public/services/palettes/palettes.test.tsx +++ b/src/plugins/charts/public/services/palettes/palettes.test.tsx @@ -18,9 +18,11 @@ */ import { coreMock } from '../../../../../core/public/mocks'; +import { createColorPalette as createLegacyColorPalette } from '../../../../../../src/plugins/charts/public'; import { PaletteDefinition } from './types'; import { buildPalettes } from './palettes'; import { colorsServiceMock } from '../legacy_colors/mock'; +import { euiPaletteColorBlind, euiPaletteColorBlindBehindText } from '@elastic/eui'; describe('palettes', () => { const palettes: Record = buildPalettes( @@ -28,79 +30,257 @@ describe('palettes', () => { colorsServiceMock ); describe('default palette', () => { - it('should return different colors based on behind text flag', () => { - const palette = palettes.default; + describe('syncColors: false', () => { + it('should return different colors based on behind text flag', () => { + const palette = palettes.default; - const color1 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - ]); - const color2 = palette.getColor( - [ + const color1 = palette.getColor([ { name: 'abc', rankAtDepth: 0, totalSeriesAtDepth: 5, }, - ], - { - behindText: true, - } - ); - expect(color1).not.toEqual(color2); - }); + ]); + const color2 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + behindText: true, + } + ); + expect(color1).not.toEqual(color2); + }); - it('should return different colors based on rank at current series', () => { - const palette = palettes.default; + it('should return different colors based on rank at current series', () => { + const palette = palettes.default; - const color1 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - ]); - const color2 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 1, - totalSeriesAtDepth: 5, - }, - ]); - expect(color1).not.toEqual(color2); + const color1 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ]); + const color2 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 1, + totalSeriesAtDepth: 5, + }, + ]); + expect(color1).not.toEqual(color2); + }); + + it('should return the same color for different positions on outer series layers', () => { + const palette = palettes.default; + + const color1 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 2, + }, + ]); + const color2 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'ghj', + rankAtDepth: 1, + totalSeriesAtDepth: 1, + }, + ]); + expect(color1).toEqual(color2); + }); }); - it('should return the same color for different positions on outer series layers', () => { - const palette = palettes.default; + describe('syncColors: true', () => { + it('should return different colors based on behind text flag', () => { + const palette = palettes.default; - const color1 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - { - name: 'def', - rankAtDepth: 0, - totalSeriesAtDepth: 2, - }, - ]); - const color2 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - { - name: 'ghj', - rankAtDepth: 1, - totalSeriesAtDepth: 1, - }, - ]); - expect(color1).toEqual(color2); + const color1 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + behindText: true, + syncColors: true, + } + ); + expect(color1).not.toEqual(color2); + }); + + it('should return different colors for different keys', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + expect(color1).not.toEqual(color2); + }); + + it('should return the same color for the same key, irregardless of rank', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'hij', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'hij', + rankAtDepth: 5, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + expect(color1).toEqual(color2); + }); + + it('should return the same color for different positions on outer series layers', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 2, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 3, + totalSeriesAtDepth: 5, + }, + { + name: 'ghj', + rankAtDepth: 1, + totalSeriesAtDepth: 1, + }, + ], + { + syncColors: true, + } + ); + expect(color1).toEqual(color2); + }); + + it('should return the same index of the behind text palette for same key', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 2, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 3, + totalSeriesAtDepth: 5, + }, + { + name: 'ghj', + rankAtDepth: 1, + totalSeriesAtDepth: 1, + }, + ], + { + syncColors: true, + behindText: true, + } + ); + const color1Index = euiPaletteColorBlind({ rotations: 2 }).indexOf(color1!); + const color2Index = euiPaletteColorBlindBehindText({ rotations: 2 }).indexOf(color2!); + expect(color1Index).toEqual(color2Index); + }); }); }); @@ -136,35 +316,87 @@ describe('palettes', () => { (colorsServiceMock.mappedColors.get as jest.Mock).mockClear(); }); - it('should query legacy color service', () => { - palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 10, - }, - ]); - expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); - expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + describe('syncColors: false', () => { + it('should not query legacy color service', () => { + palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: false, + } + ); + expect(colorsServiceMock.mappedColors.mapKeys).not.toHaveBeenCalled(); + expect(colorsServiceMock.mappedColors.get).not.toHaveBeenCalled(); + }); + + it('should return a color from the legacy palette based on position of first series', () => { + const result = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 2, + totalSeriesAtDepth: 10, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: false, + } + ); + expect(result).toEqual(createLegacyColorPalette(20)[2]); + }); }); - it('should always use root series', () => { - palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 10, - }, - { - name: 'def', - rankAtDepth: 0, - totalSeriesAtDepth: 10, - }, - ]); - expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledTimes(1); - expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); - expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledTimes(1); - expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + describe('syncColors: true', () => { + it('should query legacy color service', () => { + palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: true, + } + ); + expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); + expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + }); + + it('should always use root series', () => { + palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: true, + } + ); + expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledTimes(1); + expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); + expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledTimes(1); + expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + }); }); }); diff --git a/src/plugins/charts/public/services/palettes/palettes.tsx b/src/plugins/charts/public/services/palettes/palettes.tsx index c1fd7c3cc739f..ffb237904b36c 100644 --- a/src/plugins/charts/public/services/palettes/palettes.tsx +++ b/src/plugins/charts/public/services/palettes/palettes.tsx @@ -28,26 +28,45 @@ import { euiPaletteNegative, euiPalettePositive, euiPaletteWarm, - euiPaletteColorBlindBehindText, euiPaletteForStatus, euiPaletteForTemperature, euiPaletteComplimentary, + euiPaletteColorBlindBehindText, } from '@elastic/eui'; -import { ChartsPluginSetup } from '../../../../../../src/plugins/charts/public'; +import { flatten, zip } from 'lodash'; +import { + ChartsPluginSetup, + createColorPalette as createLegacyColorPalette, +} from '../../../../../../src/plugins/charts/public'; import { lightenColor } from './lighten_color'; import { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from './types'; import { LegacyColorsService } from '../legacy_colors'; +import { MappedColors } from '../mapped_colors'; function buildRoundRobinCategoricalWithMappedColors(): Omit { const colors = euiPaletteColorBlind({ rotations: 2 }); const behindTextColors = euiPaletteColorBlindBehindText({ rotations: 2 }); + const behindTextColorMap: Record = Object.fromEntries( + zip(colors, behindTextColors) + ); + const mappedColors = new MappedColors(undefined, (num: number) => { + return flatten(new Array(Math.ceil(num / 10)).fill(colors)).map((color) => color.toLowerCase()); + }); function getColor( series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = { behindText: false } ) { - const outputColor = chartConfiguration.behindText - ? behindTextColors[series[0].rankAtDepth % behindTextColors.length] - : colors[series[0].rankAtDepth % colors.length]; + let outputColor: string; + if (chartConfiguration.syncColors) { + const colorKey = series[0].name; + mappedColors.mapKeys([colorKey]); + const mappedColor = mappedColors.get(colorKey); + outputColor = chartConfiguration.behindText ? behindTextColorMap[mappedColor] : mappedColor; + } else { + outputColor = chartConfiguration.behindText + ? behindTextColors[series[0].rankAtDepth % behindTextColors.length] + : colors[series[0].rankAtDepth % colors.length]; + } if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) { return outputColor; @@ -115,9 +134,15 @@ function buildGradient( function buildSyncedKibanaPalette( colors: ChartsPluginSetup['legacyColors'] ): Omit { + const staticColors = createLegacyColorPalette(20); function getColor(series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = {}) { - colors.mappedColors.mapKeys([series[0].name]); - const outputColor = colors.mappedColors.get(series[0].name); + let outputColor: string; + if (chartConfiguration.syncColors) { + colors.mappedColors.mapKeys([series[0].name]); + outputColor = colors.mappedColors.get(series[0].name); + } else { + outputColor = staticColors[series[0].rankAtDepth % staticColors.length]; + } if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) { return outputColor; diff --git a/src/plugins/charts/public/services/palettes/types.ts b/src/plugins/charts/public/services/palettes/types.ts index f92bcb4bd0824..15989578518f5 100644 --- a/src/plugins/charts/public/services/palettes/types.ts +++ b/src/plugins/charts/public/services/palettes/types.ts @@ -55,6 +55,11 @@ export interface ChartColorConfiguration { * adjust colors for better a11y. Might be ignored depending on the palette. */ behindText?: boolean; + /** + * Flag whether a color assignment to a given key should be remembered and re-used the next time the key shows up. + * This setting might be ignored based on the palette. + */ + syncColors?: boolean; } /** diff --git a/src/plugins/charts/public/static/components/index.ts b/src/plugins/charts/public/static/components/index.ts index c044d361bed18..0d5d7bf3ba277 100644 --- a/src/plugins/charts/public/static/components/index.ts +++ b/src/plugins/charts/public/static/components/index.ts @@ -17,17 +17,8 @@ * under the License. */ -export { BasicOptions } from './basic_options'; export { ColorMode, LabelRotation, defaultCountLabel } from './collections'; -export { ColorRanges, SetColorRangeValue } from './color_ranges'; -export { ColorSchemaOptions, SetColorSchemaOptionsValue } from './color_schema'; export { ColorSchemaParams, Labels, Style } from './types'; -export { NumberInputOption } from './number_input'; -export { RangeOption } from './range'; -export { RequiredNumberInputOption } from './required_number_input'; -export { SelectOption } from './select'; -export { SwitchOption } from './switch'; -export { TextInputOption } from './text_input'; export { LegendToggle } from './legend_toggle'; export { ColorPicker } from './color_picker'; export { CurrentTime } from './current_time'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js b/src/plugins/console/public/application/models/legacy_core_editor/input.test.js similarity index 98% rename from src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js rename to src/plugins/console/public/application/models/legacy_core_editor/input.test.js index 81171c2bd26fe..f7b618aefd6fd 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/input.test.js @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import '../legacy_core_editor.test.mocks'; -import RowParser from '../../../../lib/row_parser'; -import { createTokenIterator } from '../../../factories'; +import './legacy_core_editor.test.mocks'; +import RowParser from '../../../lib/row_parser'; +import { createTokenIterator } from '../../factories'; import $ from 'jquery'; -import { create } from '../create'; +import { create } from './create'; describe('Input', () => { let coreEditor; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js similarity index 94% rename from src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js rename to src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js index ea7530bd21387..aa6b03e5ae290 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import '../legacy_core_editor.test.mocks'; +import './legacy_core_editor.test.mocks'; import $ from 'jquery'; -import RowParser from '../../../../lib/row_parser'; +import RowParser from '../../../lib/row_parser'; import ace from 'brace'; -import { createReadOnlyAceEditor } from '../create_readonly'; +import { createReadOnlyAceEditor } from './create_readonly'; let output; const tokenIterator = ace.acequire('ace/token_iterator'); diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt b/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt similarity index 100% rename from src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt rename to src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/integration.test.js similarity index 99% rename from src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js rename to src/plugins/console/public/application/models/sense_editor/integration.test.js index 89880528943e5..5caf772f04c39 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/integration.test.js @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import '../sense_editor.test.mocks'; -import { create } from '../create'; +import './sense_editor.test.mocks'; +import { create } from './create'; import _ from 'lodash'; import $ from 'jquery'; -import * as kb from '../../../../lib/kb/kb'; -import * as mappings from '../../../../lib/mappings/mappings'; +import * as kb from '../../../lib/kb/kb'; +import * as mappings from '../../../lib/mappings/mappings'; describe('Integration', () => { let senseEditor; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js similarity index 98% rename from src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js rename to src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index 04d3cd1a724e1..d1bc4bdd62116 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -16,14 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import '../sense_editor.test.mocks'; +import './sense_editor.test.mocks'; import $ from 'jquery'; import _ from 'lodash'; -import { create } from '../create'; -import { XJson } from '../../../../../../es_ui_shared/public'; -import editorInput1 from './editor_input1.txt'; +import { create } from './create'; +import { XJson } from '../../../../../es_ui_shared/public'; +import editorInput1 from './__fixtures__/editor_input1.txt'; const { collapseLiteralStrings } = XJson; diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js b/src/plugins/console/public/lib/autocomplete/url_autocomplete.test.js similarity index 98% rename from src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js rename to src/plugins/console/public/lib/autocomplete/url_autocomplete.test.js index 0f97416f053ee..4d2692b3ba16c 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js +++ b/src/plugins/console/public/lib/autocomplete/url_autocomplete.test.js @@ -18,13 +18,8 @@ */ import _ from 'lodash'; -import { - URL_PATH_END_MARKER, - UrlPatternMatcher, - ListComponent, -} from '../../autocomplete/components'; - -import { populateContext } from '../../autocomplete/engine'; +import { URL_PATH_END_MARKER, UrlPatternMatcher, ListComponent } from './components'; +import { populateContext } from './engine'; describe('Url autocomplete', () => { function patternsTest(name, endpoints, tokenPath, expectedContext, globalUrlComponentFactories) { diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js b/src/plugins/console/public/lib/autocomplete/url_params.test.js similarity index 96% rename from src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js rename to src/plugins/console/public/lib/autocomplete/url_params.test.js index e624e7ba57b61..d74d9c1c159bd 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js +++ b/src/plugins/console/public/lib/autocomplete/url_params.test.js @@ -17,8 +17,8 @@ * under the License. */ import _ from 'lodash'; -import { UrlParams } from '../../autocomplete/url_params'; -import { populateContext } from '../../autocomplete/engine'; +import { UrlParams } from './url_params'; +import { populateContext } from './engine'; describe('Url params', () => { function paramTest(name, description, tokenPath, expectedContext, globalParams) { diff --git a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.txt b/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt similarity index 100% rename from src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.txt rename to src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt diff --git a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js b/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js similarity index 93% rename from src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js rename to src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js index 068dd68be4ba8..6f4e531715f7f 100644 --- a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js +++ b/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js @@ -18,8 +18,8 @@ */ import _ from 'lodash'; -import { detectCURL, parseCURL } from '../curl'; -import curlTests from './curl_parsing.txt'; +import { detectCURL, parseCURL } from './curl'; +import curlTests from './__fixtures__/curl_parsing.txt'; describe('CURL', () => { const notCURLS = ['sldhfsljfhs', 's;kdjfsldkfj curl -XDELETE ""', '{ "hello": 1 }']; diff --git a/src/plugins/console/public/lib/es/__tests__/content_type.test.js b/src/plugins/console/public/lib/es/content_type.test.js similarity index 96% rename from src/plugins/console/public/lib/es/__tests__/content_type.test.js rename to src/plugins/console/public/lib/es/content_type.test.js index e800fe41cb018..af62a3cad3f1f 100644 --- a/src/plugins/console/public/lib/es/__tests__/content_type.test.js +++ b/src/plugins/console/public/lib/es/content_type.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { getContentType } from '../es'; +import { getContentType } from './es'; const APPLICATION_JSON = 'application/json'; describe('Content type', () => { diff --git a/src/plugins/console/public/lib/kb/__tests__/kb.test.js b/src/plugins/console/public/lib/kb/kb.test.js similarity index 96% rename from src/plugins/console/public/lib/kb/__tests__/kb.test.js rename to src/plugins/console/public/lib/kb/kb.test.js index eaf5023053880..a7e43f2e94a50 100644 --- a/src/plugins/console/public/lib/kb/__tests__/kb.test.js +++ b/src/plugins/console/public/lib/kb/kb.test.js @@ -18,11 +18,11 @@ */ import _ from 'lodash'; -import { populateContext } from '../../autocomplete/engine'; +import { populateContext } from '../autocomplete/engine'; -import '../../../application/models/sense_editor/sense_editor.test.mocks'; -import * as kb from '../../kb'; -import * as mappings from '../../mappings/mappings'; +import '../../application/models/sense_editor/sense_editor.test.mocks'; +import * as kb from '../kb'; +import * as mappings from '../mappings/mappings'; describe('Knowledge base', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js b/src/plugins/console/public/lib/mappings/mapping.test.js similarity index 98% rename from src/plugins/console/public/lib/mappings/__tests__/mapping.test.js rename to src/plugins/console/public/lib/mappings/mapping.test.js index ce52b060f418f..ab4c08fca1553 100644 --- a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js +++ b/src/plugins/console/public/lib/mappings/mapping.test.js @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import '../../../application/models/sense_editor/sense_editor.test.mocks'; -import * as mappings from '../mappings'; +import '../../application/models/sense_editor/sense_editor.test.mocks'; +import * as mappings from './mappings'; describe('Mappings', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/utils/__tests__/utils.test.js b/src/plugins/console/public/lib/utils/utils.test.js similarity index 99% rename from src/plugins/console/public/lib/utils/__tests__/utils.test.js rename to src/plugins/console/public/lib/utils/utils.test.js index e47e71c742a81..ee86756da8362 100644 --- a/src/plugins/console/public/lib/utils/__tests__/utils.test.js +++ b/src/plugins/console/public/lib/utils/utils.test.js @@ -17,7 +17,7 @@ * under the License. */ -import * as utils from '../'; +import * as utils from '.'; describe('Utils class', () => { test('extract deprecation messages', function () { diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts b/src/plugins/console/server/routes/api/console/proxy/body.test.ts similarity index 80% rename from src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts rename to src/plugins/console/server/routes/api/console/proxy/body.test.ts index d0c8383792796..b6ba08c13b06b 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/body.test.ts @@ -18,12 +18,11 @@ */ import { getProxyRouteHandlerDeps } from './mocks'; -import expect from '@kbn/expect'; import { Readable } from 'stream'; -import { kibanaResponseFactory } from '../../../../../../../../core/server'; -import { createHandler } from '../create_handler'; -import * as requestModule from '../../../../../lib/proxy_request'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; +import { createHandler } from './create_handler'; +import * as requestModule from '../../../../lib/proxy_request'; import { createResponseStub } from './stubs'; describe('Console Proxy Route', () => { @@ -62,38 +61,38 @@ describe('Console Proxy Route', () => { describe('GET request', () => { it('returns the exact body', async () => { const { payload } = await request('GET', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('POST request', () => { it('returns the exact body', async () => { const { payload } = await request('POST', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('PUT request', () => { it('returns the exact body', async () => { const { payload } = await request('PUT', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('DELETE request', () => { it('returns the exact body', async () => { const { payload } = await request('DELETE', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('HEAD request', () => { it('returns the status code and text', async () => { const { payload } = await request('HEAD', '/'); - expect(typeof payload).to.be('string'); - expect(payload).to.be('200 - OK'); + expect(typeof payload).toBe('string'); + expect(payload).toBe('200 - OK'); }); describe('mixed casing', () => { it('returns the status code and text', async () => { const { payload } = await request('HeAd', '/'); - expect(typeof payload).to.be('string'); - expect(payload).to.be('200 - OK'); + expect(typeof payload).toBe('string'); + expect(payload).toBe('200 - OK'); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts b/src/plugins/console/server/routes/api/console/proxy/headers.test.ts similarity index 71% rename from src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts rename to src/plugins/console/server/routes/api/console/proxy/headers.test.ts index 2d4c616754e33..5ea08e7ada9ba 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/headers.test.ts @@ -16,21 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -jest.mock('../../../../../../../../core/server/http/router/request', () => ({ +jest.mock('../../../../../../../core/server/http/router/request', () => ({ ensureRawRequest: jest.fn(), })); -import { kibanaResponseFactory } from '../../../../../../../../core/server'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ensureRawRequest } from '../../../../../../../../core/server/http/router/request'; +import { ensureRawRequest } from '../../../../../../../core/server/http/router/request'; import { getProxyRouteHandlerDeps } from './mocks'; -import expect from '@kbn/expect'; -import * as requestModule from '../../../../../lib/proxy_request'; +import * as requestModule from '../../../../lib/proxy_request'; -import { createHandler } from '../create_handler'; +import { createHandler } from './create_handler'; import { createResponseStub } from './stubs'; @@ -74,16 +73,16 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[{ headers }]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(headers).to.have.property('x-forwarded-for'); - expect(headers['x-forwarded-for']).to.be('0.0.0.0'); - expect(headers).to.have.property('x-forwarded-port'); - expect(headers['x-forwarded-port']).to.be('1234'); - expect(headers).to.have.property('x-forwarded-proto'); - expect(headers['x-forwarded-proto']).to.be('http'); - expect(headers).to.have.property('x-forwarded-host'); - expect(headers['x-forwarded-host']).to.be('test'); + expect(headers).toHaveProperty('x-forwarded-for'); + expect(headers['x-forwarded-for']).toBe('0.0.0.0'); + expect(headers).toHaveProperty('x-forwarded-port'); + expect(headers['x-forwarded-port']).toBe('1234'); + expect(headers).toHaveProperty('x-forwarded-proto'); + expect(headers['x-forwarded-proto']).toBe('http'); + expect(headers).toHaveProperty('x-forwarded-host'); + expect(headers['x-forwarded-host']).toBe('test'); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts b/src/plugins/console/server/routes/api/console/proxy/mocks.ts similarity index 92% rename from src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts rename to src/plugins/console/server/routes/api/console/proxy/mocks.ts index 158a4a979683f..4d55a27d7aa2f 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts +++ b/src/plugins/console/server/routes/api/console/proxy/mocks.ts @@ -17,15 +17,15 @@ * under the License. */ -jest.mock('../../../../../lib/proxy_request', () => ({ +jest.mock('../../../../lib/proxy_request', () => ({ proxyRequest: jest.fn(), })); import { duration } from 'moment'; -import { ProxyConfigCollection } from '../../../../../lib'; -import { RouteDependencies, ProxyDependencies } from '../../../../../routes'; -import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../../services'; -import { coreMock, httpServiceMock } from '../../../../../../../../core/server/mocks'; +import { ProxyConfigCollection } from '../../../../lib'; +import { RouteDependencies, ProxyDependencies } from '../../../../routes'; +import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../services'; +import { coreMock, httpServiceMock } from '../../../../../../../core/server/mocks'; const defaultProxyValue = Object.freeze({ readLegacyESConfig: async () => ({ diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts b/src/plugins/console/server/routes/api/console/proxy/params.test.ts similarity index 87% rename from src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts rename to src/plugins/console/server/routes/api/console/proxy/params.test.ts index fc1dae7fbcea2..8838fa405b88f 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/params.test.ts @@ -16,13 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { kibanaResponseFactory } from '../../../../../../../../core/server'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; import { getProxyRouteHandlerDeps } from './mocks'; import { createResponseStub } from './stubs'; -import * as requestModule from '../../../../../lib/proxy_request'; -import expect from '@kbn/expect'; +import * as requestModule from '../../../../lib/proxy_request'; -import { createHandler } from '../create_handler'; +import { createHandler } from './create_handler'; describe('Console Proxy Route', () => { let handler: ReturnType; @@ -45,7 +44,7 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect(status).to.be(403); + expect(status).toBe(403); }); }); describe('one match', () => { @@ -62,8 +61,8 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect(status).to.be(200); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect(status).toBe(200); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); }); }); describe('all match', () => { @@ -80,8 +79,8 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect(status).to.be(200); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect(status).toBe(200); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts b/src/plugins/console/server/routes/api/console/proxy/proxy_fallback.test.ts similarity index 92% rename from src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts rename to src/plugins/console/server/routes/api/console/proxy/proxy_fallback.test.ts index 17ce715ac1afa..b9575b7abeea3 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/proxy_fallback.test.ts @@ -20,9 +20,9 @@ import { duration } from 'moment'; import { getProxyRouteHandlerDeps } from './mocks'; -import { kibanaResponseFactory } from '../../../../../../../../core/server'; -import * as requestModule from '../../../../../lib/proxy_request'; -import { createHandler } from '../create_handler'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; +import * as requestModule from '../../../../lib/proxy_request'; +import { createHandler } from './create_handler'; describe('Console Proxy Route', () => { afterEach(async () => { diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts b/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts similarity index 81% rename from src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts rename to src/plugins/console/server/routes/api/console/proxy/query_string.test.ts index f0e7e5d6e8f9a..7b7bd6b605d96 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts @@ -16,14 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { kibanaResponseFactory } from '../../../../../../../../core/server'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; import { getProxyRouteHandlerDeps } from './mocks'; import { createResponseStub } from './stubs'; -import * as requestModule from '../../../../../lib/proxy_request'; +import * as requestModule from '../../../../lib/proxy_request'; -import expect from '@kbn/expect'; - -import { createHandler } from '../create_handler'; +import { createHandler } from './create_handler'; describe('Console Proxy Route', () => { let request: any; @@ -50,25 +48,25 @@ describe('Console Proxy Route', () => { describe('contains full url', () => { it('treats the url as a path', async () => { await request('GET', 'http://evil.com/test'); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(args.uri.href).to.be('http://localhost:9200/http://evil.com/test?pretty=true'); + expect(args.uri.href).toBe('http://localhost:9200/http://evil.com/test?pretty=true'); }); }); describe('starts with a slash', () => { it('combines well with the base url', async () => { await request('GET', '/index/id'); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(args.uri.href).to.be('http://localhost:9200/index/id?pretty=true'); + expect(args.uri.href).toBe('http://localhost:9200/index/id?pretty=true'); }); }); describe(`doesn't start with a slash`, () => { it('combines well with the base url', async () => { await request('GET', 'index/id'); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(args.uri.href).to.be('http://localhost:9200/index/id?pretty=true'); + expect(args.uri.href).toBe('http://localhost:9200/index/id?pretty=true'); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/route_validation.test.ts b/src/plugins/console/server/routes/api/console/proxy/route_validation.test.ts similarity index 96% rename from src/plugins/console/server/routes/api/console/proxy/tests/route_validation.test.ts rename to src/plugins/console/server/routes/api/console/proxy/route_validation.test.ts index 2588c96e3b091..a67c742f09fb5 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/route_validation.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/route_validation.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { routeValidationConfig } from '../validation_config'; +import { routeValidationConfig } from './validation_config'; const { query } = routeValidationConfig; diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/stubs.ts b/src/plugins/console/server/routes/api/console/proxy/stubs.ts similarity index 100% rename from src/plugins/console/server/routes/api/console/proxy/tests/stubs.ts rename to src/plugins/console/server/routes/api/console/proxy/stubs.ts diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 880d40cc3c612..e0d59ef2a17b6 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -19,7 +19,7 @@ import _ from 'lodash'; -import { ActionByType, IncompatibleActionError } from '../../services/ui_actions'; +import { Action, IncompatibleActionError } from '../../services/ui_actions'; import { ViewMode, PanelState, @@ -39,7 +39,7 @@ export interface AddToLibraryActionContext { embeddable: IEmbeddable; } -export class AddToLibraryAction implements ActionByType { +export class AddToLibraryAction implements Action { public readonly type = ACTION_ADD_TO_LIBRARY; public readonly id = ACTION_ADD_TO_LIBRARY; public order = 15; diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index d27e2d6dce651..e59cefed598e8 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -21,7 +21,7 @@ import _ from 'lodash'; import uuid from 'uuid'; import { CoreStart } from 'src/core/public'; -import { ActionByType, IncompatibleActionError } from '../../services/ui_actions'; +import { Action, IncompatibleActionError } from '../../services/ui_actions'; import { SavedObject } from '../../services/saved_objects'; import { ViewMode, @@ -45,7 +45,7 @@ export interface ClonePanelActionContext { embeddable: IEmbeddable; } -export class ClonePanelAction implements ActionByType { +export class ClonePanelAction implements Action { public readonly type = ACTION_CLONE_PANEL; public readonly id = ACTION_CLONE_PANEL; public order = 45; diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx index fe14ce13d44bc..3336f8dc5915c 100644 --- a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx @@ -19,7 +19,7 @@ import { dashboardExpandPanelAction } from '../../dashboard_strings'; import { IEmbeddable } from '../../services/embeddable'; -import { ActionByType, IncompatibleActionError } from '../../services/ui_actions'; +import { Action, IncompatibleActionError } from '../../services/ui_actions'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, @@ -46,7 +46,7 @@ export interface ExpandPanelActionContext { embeddable: IEmbeddable; } -export class ExpandPanelAction implements ActionByType { +export class ExpandPanelAction implements Action { public readonly type = ACTION_EXPAND_PANEL; public readonly id = ACTION_EXPAND_PANEL; public order = 7; diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index a31ecbea88bab..1048ce2189744 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -24,7 +24,7 @@ import { FormatFactory } from '../../../../data/common/field_formats/utils'; import { DataPublicPluginStart, exporters } from '../../services/data'; import { downloadMultipleAs } from '../../services/share'; import { Adapters, IEmbeddable } from '../../services/embeddable'; -import { ActionByType } from '../../services/ui_actions'; +import { Action } from '../../services/ui_actions'; import { dashboardExportCsvAction } from '../../dashboard_strings'; export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; @@ -44,7 +44,7 @@ export interface ExportContext { * This is "Export CSV" action which appears in the context * menu of a dashboard panel. */ -export class ExportCSVAction implements ActionByType { +export class ExportCSVAction implements Action { public readonly id = ACTION_EXPORT_CSV; public readonly type = ACTION_EXPORT_CSV; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index 13ccb279df821..a6d718846b9fe 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -19,7 +19,7 @@ import React from 'react'; -import { ActionByType, IncompatibleActionError } from '../../services/ui_actions'; +import { Action, IncompatibleActionError } from '../../services/ui_actions'; import { reactToUiComponent } from '../../services/kibana_react'; import { IEmbeddable, @@ -38,7 +38,7 @@ export interface LibraryNotificationActionContext { embeddable: IEmbeddable; } -export class LibraryNotificationAction implements ActionByType { +export class LibraryNotificationAction implements Action { public readonly id = ACTION_LIBRARY_NOTIFICATION; public readonly type = ACTION_LIBRARY_NOTIFICATION; public readonly order = 1; diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx index 553a0b9770d01..909b758c511a9 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx @@ -20,7 +20,7 @@ import { CoreStart } from 'src/core/public'; import { IEmbeddable, ViewMode, EmbeddableStart } from '../../services/embeddable'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; -import { ActionByType, IncompatibleActionError } from '../../services/ui_actions'; +import { Action, IncompatibleActionError } from '../../services/ui_actions'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; import { dashboardReplacePanelAction } from '../../dashboard_strings'; @@ -34,7 +34,7 @@ export interface ReplacePanelActionContext { embeddable: IEmbeddable; } -export class ReplacePanelAction implements ActionByType { +export class ReplacePanelAction implements Action { public readonly type = ACTION_REPLACE_PANEL; public readonly id = ACTION_REPLACE_PANEL; public order = 3; diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx index 93ceb72624259..901367b6af833 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { ActionByType, IncompatibleActionError } from '../../services/ui_actions'; +import { Action, IncompatibleActionError } from '../../services/ui_actions'; import { ViewMode, PanelState, @@ -38,7 +38,7 @@ export interface UnlinkFromLibraryActionContext { embeddable: IEmbeddable; } -export class UnlinkFromLibraryAction implements ActionByType { +export class UnlinkFromLibraryAction implements Action { public readonly type = ACTION_UNLINK_FROM_LIBRARY; public readonly id = ACTION_UNLINK_FROM_LIBRARY; public order = 15; diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 845d64c16500d..f33383427342b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -74,7 +74,10 @@ export function DashboardApp({ const [indexPatterns, setIndexPatterns] = useState([]); const savedDashboard = useSavedDashboard(savedDashboardId, history); - const dashboardStateManager = useDashboardStateManager(savedDashboard, history); + const { dashboardStateManager, viewMode, setViewMode } = useDashboardStateManager( + savedDashboard, + history + ); const dashboardContainer = useDashboardContainer(dashboardStateManager, history, false); const refreshDashboardContainer = useCallback( @@ -113,6 +116,10 @@ export function DashboardApp({ removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true); } + if (changes.viewMode) { + setViewMode(changes.viewMode); + } + dashboardContainer.updateInput({ ...changes, // do not start a new session if this is irrelevant state change to prevent excessive searches @@ -123,6 +130,7 @@ export function DashboardApp({ [ history, data.query, + setViewMode, embedSettings, dashboardContainer, data.search.session, @@ -222,7 +230,7 @@ export function DashboardApp({ return (
- {savedDashboard && dashboardStateManager && dashboardContainer && ( + {savedDashboard && dashboardStateManager && dashboardContainer && viewMode && ( <> { diff --git a/src/plugins/dashboard/public/application/dashboard_app_functions.ts b/src/plugins/dashboard/public/application/dashboard_app_functions.ts index 0381fdb2e55b5..af7a485296ea0 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_functions.ts +++ b/src/plugins/dashboard/public/application/dashboard_app_functions.ts @@ -151,6 +151,7 @@ export const getDashboardContainerInput = ({ description: dashboardStateManager.getDescription(), id: dashboardStateManager.savedDashboard.id || '', useMargins: dashboardStateManager.getUseMargins(), + syncColors: dashboardStateManager.getSyncColors(), viewMode: dashboardStateManager.getViewMode(), filters: query.filterManager.getFilters(), query: dashboardStateManager.getQuery(), diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index 8ae606854374a..71fe948485893 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { parse, ParsedQuery } from 'query-string'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Switch, Route, RouteComponentProps, HashRouter } from 'react-router-dom'; +import { Switch, Route, RouteComponentProps, HashRouter, Redirect } from 'react-router-dom'; import { DashboardListing } from './listing'; import { DashboardApp } from './dashboard_app'; @@ -41,6 +41,7 @@ import { PluginInitializerContext, ScopedHistory, } from '../services/core'; +import { DashboardNoMatch } from './listing/dashboard_no_match'; export const dashboardUrlParams = { showTopMenu: 'show-top-menu', @@ -77,6 +78,7 @@ export async function mountApp({ const { navigation, savedObjects, + urlForwarding, data: dataStart, share: shareStart, embeddable: embeddableStart, @@ -88,6 +90,7 @@ export async function mountApp({ navigation, onAppLeave, savedObjects, + urlForwarding, usageCollection, core: coreStart, data: dataStart, @@ -181,6 +184,10 @@ export async function mountApp({ ); }; + const renderNoMatch = (routeProps: RouteComponentProps) => { + return ; + }; + // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); @@ -203,6 +210,10 @@ export async function mountApp({ render={renderDashboard} /> + + + + diff --git a/src/plugins/dashboard/public/application/dashboard_state.test.ts b/src/plugins/dashboard/public/application/dashboard_state.test.ts index 3cff8ff03be9a..a763b6f9d1d4a 100644 --- a/src/plugins/dashboard/public/application/dashboard_state.test.ts +++ b/src/plugins/dashboard/public/application/dashboard_state.test.ts @@ -69,6 +69,7 @@ describe('DashboardState', function () { query: {} as DashboardContainerInput['query'], timeRange: {} as DashboardContainerInput['timeRange'], useMargins: true, + syncColors: false, title: 'ultra awesome test dashboard', isFullScreenMode: false, panels: {} as DashboardContainerInput['panels'], diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 9e3f23f7dedf9..a0bdef68a9f8f 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -419,6 +419,15 @@ export class DashboardStateManager { this.stateContainer.transitions.setOption('useMargins', useMargins); } + public getSyncColors() { + // Existing dashboards that don't define this should default to true. + return this.appState.options.syncColors === undefined ? true : this.appState.options.syncColors; + } + + public setSyncColors(syncColors: boolean) { + this.stateContainer.transitions.setOption('syncColors', syncColors); + } + public getHidePanelTitles() { return this.appState.options.hidePanelTitles; } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 01b4e81fc484c..a3b67ede9f3f9 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -59,6 +59,7 @@ export interface DashboardContainerInput extends ContainerInput { timeRange: TimeRange; description?: string; useMargins: boolean; + syncColors?: boolean; viewMode: ViewMode; filters: Filter[]; title: string; @@ -93,6 +94,7 @@ export interface InheritedChildInput extends IndexSignature { hidePanelTitles?: boolean; id: string; searchSessionId?: string; + syncColors?: boolean; } export type DashboardReactContextValue = KibanaReactContextValue; @@ -269,6 +271,7 @@ export class DashboardContainer extends Container
false` function defaultTaggingGuard(_obj: SavedObject): _obj is TagDecoratedSavedObject { return false; } +interface DashboardStateManagerReturn { + dashboardStateManager: DashboardStateManager | null; + viewMode: ViewMode | null; + setViewMode: (value: ViewMode) => void; +} + export const useDashboardStateManager = ( savedDashboard: DashboardSavedObject | null, history: History -): DashboardStateManager | null => { +): DashboardStateManagerReturn => { const { data: dataPlugin, core, @@ -74,6 +81,7 @@ export const useDashboardStateManager = ( const [dashboardStateManager, setDashboardStateManager] = useState( null ); + const [viewMode, setViewMode] = useState(null); const hasTaggingCapabilities = savedObjectsTagging?.ui.hasTagDecoration || defaultTaggingGuard; @@ -183,6 +191,7 @@ export const useDashboardStateManager = ( } setDashboardStateManager(stateManager); + setViewMode(stateManager.getViewMode()); return () => { stateManager?.destroy(); @@ -209,5 +218,5 @@ export const useDashboardStateManager = ( usageCollection, ]); - return dashboardStateManager; + return { dashboardStateManager, viewMode, setViewMode }; }; diff --git a/src/plugins/dashboard/public/application/lib/help_menu_util.ts b/src/plugins/dashboard/public/application/lib/help_menu_util.ts index efdff051a25a0..ee06e7bc5ecf2 100644 --- a/src/plugins/dashboard/public/application/lib/help_menu_util.ts +++ b/src/plugins/dashboard/public/application/lib/help_menu_util.ts @@ -31,7 +31,7 @@ export function addHelpMenuToAppChrome( links: [ { linkType: 'documentation', - href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/dashboard.html`, + href: `${docLinks.links.dashboard.guide}`, }, ], }); diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap index fad7d8ddaabfe..bce8a661634f6 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -126,7 +126,7 @@ exports[`after fetch When given a title that matches multiple dashboards, filter restrictWidth={true} >
@@ -218,7 +218,7 @@ exports[`after fetch hideWriteControls 1`] = ` restrictWidth={true} >
@@ -358,7 +358,7 @@ exports[`after fetch initialFilter 1`] = ` restrictWidth={true} >
@@ -497,7 +497,7 @@ exports[`after fetch renders all table rows 1`] = ` restrictWidth={true} >
@@ -636,7 +636,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` restrictWidth={true} >
@@ -775,7 +775,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` restrictWidth={true} >
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index 3aee05554b0d9..8172be46e9f3a 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -39,6 +39,7 @@ import { dataPluginMock } from '../../../../data/public/mocks'; import { chromeServiceMock, coreMock } from '../../../../../core/public/mocks'; import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; +import { UrlForwardingStart } from '../../../../url_forwarding/public'; function makeDefaultServices(): DashboardAppServices { const core = coreMock.createStart(); @@ -71,6 +72,7 @@ function makeDefaultServices(): DashboardAppServices { scopedHistory: () => ({} as ScopedHistory), savedQueryService: {} as SavedQueryService, setHeaderActionMenu: (mountPoint) => {}, + urlForwarding: {} as UrlForwardingStart, uiSettings: {} as IUiSettingsClient, restorePreviousUrl: () => {}, onAppLeave: (handler) => {}, diff --git a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx new file mode 100644 index 0000000000000..a0f13af92ff77 --- /dev/null +++ b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx @@ -0,0 +1,76 @@ +/* + * 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, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut } from '@elastic/eui'; + +import { RouteComponentProps } from 'react-router-dom'; +import { useKibana, toMountPoint } from '../../services/kibana_react'; +import { DashboardAppServices } from '../types'; +import { DashboardConstants } from '../..'; + +let bannerId: string | undefined; + +export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['history'] }) => { + const { services } = useKibana(); + + useEffect(() => { + services.restorePreviousUrl(); + + const { navigated } = services.urlForwarding.navigateToLegacyKibanaUrl( + history.location.pathname + ); + + if (!navigated) { + const bannerMessage = i18n.translate('dashboard.noMatchRoute.bannerTitleText', { + defaultMessage: 'Page not found', + }); + + bannerId = services.core.overlays.banners.replace( + bannerId, + toMountPoint( + +

+ +

+
+ ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + setTimeout(() => { + if (bannerId) { + services.core.overlays.banners.remove(bannerId); + } + }, 15000); + + history.replace(DashboardConstants.LANDING_PAGE_PATH); + } + }, [services, history]); + + return null; +}; diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 6043dc1874687..e0f516224c230 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -55,10 +55,12 @@ import { TopNavIds } from './top_nav_ids'; import { ShowShareModal } from './show_share_modal'; import { PanelToolbar } from './panel_toolbar'; import { confirmDiscardUnsavedChanges } from '../listing/discard_changes_confirm'; +import { OverlayRef } from '../../../../../core/public'; import { DashboardContainer } from '..'; export interface DashboardTopNavState { chromeIsVisible: boolean; + addPanelOverlay?: OverlayRef; savedQuery?: SavedQuery; } @@ -72,6 +74,7 @@ export interface DashboardTopNavProps { indexPatterns: IndexPattern[]; redirectTo: DashboardRedirect; lastDashboardId?: string; + viewMode: ViewMode; } export function DashboardTopNav({ @@ -84,6 +87,7 @@ export function DashboardTopNav({ indexPatterns, redirectTo, timefilter, + viewMode, }: DashboardTopNavProps) { const { core, @@ -109,14 +113,17 @@ export function DashboardTopNav({ const addFromLibrary = useCallback(() => { if (!isErrorEmbeddable(dashboardContainer)) { - openAddPanelFlyout({ - embeddable: dashboardContainer, - getAllFactories: embeddable.getEmbeddableFactories, - getFactory: embeddable.getEmbeddableFactory, - notifications: core.notifications, - overlays: core.overlays, - SavedObjectFinder: getSavedObjectFinder(core.savedObjects, uiSettings), - }); + setState((s) => ({ + ...s, + addPanelOverlay: openAddPanelFlyout({ + embeddable: dashboardContainer, + getAllFactories: embeddable.getEmbeddableFactories, + getFactory: embeddable.getEmbeddableFactory, + notifications: core.notifications, + overlays: core.overlays, + SavedObjectFinder: getSavedObjectFinder(core.savedObjects, uiSettings), + }), + })); } }, [ embeddable.getEmbeddableFactories, @@ -137,8 +144,16 @@ export function DashboardTopNav({ await factory.create({} as EmbeddableInput, dashboardContainer); }, [dashboardContainer, embeddable]); + const clearAddPanel = useCallback(() => { + if (state.addPanelOverlay) { + state.addPanelOverlay.close(); + setState((s) => ({ ...s, addPanelOverlay: undefined })); + } + }, [state.addPanelOverlay]); + const onChangeViewMode = useCallback( (newMode: ViewMode) => { + clearAddPanel(); const isPageRefresh = newMode === dashboardStateManager.getViewMode(); const isLeavingEditMode = !isPageRefresh && newMode === ViewMode.VIEW; const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter); @@ -169,7 +184,7 @@ export function DashboardTopNav({ confirmDiscardUnsavedChanges(core.overlays, revertChangesAndExitEditMode); }, - [redirectTo, timefilter, core.overlays, savedDashboard.id, dashboardStateManager] + [redirectTo, timefilter, core.overlays, savedDashboard.id, dashboardStateManager, clearAddPanel] ); /** @@ -293,8 +308,16 @@ export function DashboardTopNav({ showCopyOnSave={lastDashboardId ? true : false} /> ); + clearAddPanel(); showSaveModal(dashboardSaveModal, core.i18n.Context); - }, [save, core.i18n.Context, savedObjectsTagging, dashboardStateManager, lastDashboardId]); + }, [ + save, + clearAddPanel, + lastDashboardId, + core.i18n.Context, + savedObjectsTagging, + dashboardStateManager, + ]); const runClone = useCallback(() => { const currentTitle = dashboardStateManager.getTitle(); @@ -340,6 +363,10 @@ export function DashboardTopNav({ onUseMarginsChange: (isChecked: boolean) => { dashboardStateManager.setUseMargins(isChecked); }, + syncColors: dashboardStateManager.getSyncColors(), + onSyncColorsChange: (isChecked: boolean) => { + dashboardStateManager.setSyncColors(isChecked); + }, hidePanelTitles: dashboardStateManager.getHidePanelTitles(), onHidePanelTitlesChange: (isChecked: boolean) => { dashboardStateManager.setHidePanelTitles(isChecked); @@ -389,7 +416,7 @@ export function DashboardTopNav({ const showSearchBar = showQueryBar || showFilterBar; const topNav = getTopNavConfig( - dashboardStateManager.getViewMode(), + viewMode, dashboardTopNavActions, dashboardCapabilities.hideWriteControls ); @@ -436,7 +463,7 @@ export function DashboardTopNav({ return ( <> - {!dashboardStateManager.getIsViewMode() ? ( + {viewMode !== ViewMode.VIEW ? ( ) : null} diff --git a/src/plugins/dashboard/public/application/top_nav/options.tsx b/src/plugins/dashboard/public/application/top_nav/options.tsx index 3398696ff40db..86409cdeba74f 100644 --- a/src/plugins/dashboard/public/application/top_nav/options.tsx +++ b/src/plugins/dashboard/public/application/top_nav/options.tsx @@ -27,17 +27,21 @@ interface Props { onUseMarginsChange: (useMargins: boolean) => void; hidePanelTitles: boolean; onHidePanelTitlesChange: (hideTitles: boolean) => void; + syncColors: boolean; + onSyncColorsChange: (syncColors: boolean) => void; } interface State { useMargins: boolean; hidePanelTitles: boolean; + syncColors: boolean; } export class OptionsMenu extends Component { state = { useMargins: this.props.useMargins, hidePanelTitles: this.props.hidePanelTitles, + syncColors: this.props.syncColors, }; constructor(props: Props) { @@ -56,6 +60,12 @@ export class OptionsMenu extends Component { this.setState({ hidePanelTitles: isChecked }); }; + handleSyncColorsChange = (evt: any) => { + const isChecked = evt.target.checked; + this.props.onSyncColorsChange(isChecked); + this.setState({ syncColors: isChecked }); + }; + render() { return ( @@ -80,6 +90,17 @@ export class OptionsMenu extends Component { data-test-subj="dashboardPanelTitlesCheckbox" /> + + + + ); } diff --git a/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx b/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx index 7c23e4808fbea..6c519ccad327f 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx @@ -39,10 +39,14 @@ export function showOptionsPopover({ onUseMarginsChange, hidePanelTitles, onHidePanelTitlesChange, + syncColors, + onSyncColorsChange, }: { anchorElement: HTMLElement; useMargins: boolean; onUseMarginsChange: (useMargins: boolean) => void; + syncColors: boolean; + onSyncColorsChange: (syncColors: boolean) => void; hidePanelTitles: boolean; onHidePanelTitlesChange: (hideTitles: boolean) => void; }) { @@ -62,6 +66,8 @@ export function showOptionsPopover({ onUseMarginsChange={onUseMarginsChange} hidePanelTitles={hidePanelTitles} onHidePanelTitlesChange={onHidePanelTitlesChange} + syncColors={syncColors} + onSyncColorsChange={onSyncColorsChange} /> diff --git a/src/plugins/dashboard/public/application/types.ts b/src/plugins/dashboard/public/application/types.ts index 2db20315e7d73..0f652ddb0aa4a 100644 --- a/src/plugins/dashboard/public/application/types.ts +++ b/src/plugins/dashboard/public/application/types.ts @@ -34,6 +34,7 @@ import { SavedObjectsTaggingApi } from '../services/saved_objects_tagging_oss'; import { DataPublicPluginStart, IndexPatternsContract } from '../services/data'; import { SavedObjectLoader, SavedObjectsStart } from '../services/saved_objects'; import { DashboardPanelStorage } from './lib'; +import { UrlForwardingStart } from '../../../url_forwarding/public'; export type DashboardRedirect = (props: RedirectToProps) => void; export type RedirectToProps = @@ -76,6 +77,7 @@ export interface DashboardAppServices { uiSettings: IUiSettingsClient; restorePreviousUrl: () => void; savedObjects: SavedObjectsStart; + urlForwarding: UrlForwardingStart; savedDashboards: SavedObjectLoader; scopedHistory: () => ScopedHistory; indexPatterns: IndexPatternsContract; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 54c522d97a13e..b9b8e9be885d8 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -62,27 +62,15 @@ import { } from './services/kibana_react'; import { - ACTION_CLONE_PANEL, - ACTION_EXPAND_PANEL, - ACTION_REPLACE_PANEL, ClonePanelAction, - ClonePanelActionContext, createDashboardContainerByValueRenderer, DASHBOARD_CONTAINER_TYPE, DashboardContainerFactory, DashboardContainerFactoryDefinition, ExpandPanelAction, - ExpandPanelActionContext, ReplacePanelAction, - ReplacePanelActionContext, - ACTION_UNLINK_FROM_LIBRARY, - UnlinkFromLibraryActionContext, UnlinkFromLibraryAction, - ACTION_ADD_TO_LIBRARY, - AddToLibraryActionContext, AddToLibraryAction, - ACTION_LIBRARY_NOTIFICATION, - LibraryNotificationActionContext, LibraryNotificationAction, } from './application'; import { @@ -94,11 +82,7 @@ import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; -import { - ACTION_EXPORT_CSV, - ExportContext, - ExportCSVAction, -} from './application/actions/export_csv_action'; +import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; declare module '../../share/public' { @@ -147,18 +131,6 @@ export interface DashboardStart { DashboardContainerByValueRenderer: ReturnType; } -declare module '../../../plugins/ui_actions/public' { - export interface ActionContextMapping { - [ACTION_EXPAND_PANEL]: ExpandPanelActionContext; - [ACTION_REPLACE_PANEL]: ReplacePanelActionContext; - [ACTION_CLONE_PANEL]: ClonePanelActionContext; - [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; - [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; - [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; - [ACTION_EXPORT_CSV]: ExportContext; - } -} - export class DashboardPlugin implements Plugin { diff --git a/src/plugins/dashboard/public/services/ui_actions.ts b/src/plugins/dashboard/public/services/ui_actions.ts index 4c9ac590191f6..81bdad35d464c 100644 --- a/src/plugins/dashboard/public/services/ui_actions.ts +++ b/src/plugins/dashboard/public/services/ui_actions.ts @@ -18,7 +18,7 @@ */ export { - ActionByType, + Action, IncompatibleActionError, UiActionsSetup, UiActionsStart, diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index af0aed9203581..af0b850c0ff80 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -78,6 +78,7 @@ export interface DashboardAppState { options: { hidePanelTitles: boolean; useMargins: boolean; + syncColors?: boolean; }; query: Query | string; filters: Filter[]; diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 13bb8443ffef6..2448d5f22ced2 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -1,6 +1,6 @@ --- id: kibDataPlugin -slug: /kibana-dev-guide/services/data-plugin +slug: /kibana-dev-docs/services/data-plugin title: Data services image: https://source.unsplash.com/400x175/?Search summary: The data plugin contains services for searching, querying and filtering. diff --git a/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js b/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js new file mode 100644 index 0000000000000..e421877724f56 --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { shouldReadFieldFromDocValues, castEsToKbnFieldTypeName } from '../../../../server'; + +function stubbedLogstashFields() { + return [ + // |aggregatable + // | |searchable + // name esType | | |metadata | subType + ['bytes', 'long', true, true, { count: 10 }], + ['ssl', 'boolean', true, true, { count: 20 }], + ['@timestamp', 'date', true, true, { count: 30 }], + ['time', 'date', true, true, { count: 30 }], + ['@tags', 'keyword', true, true], + ['utc_time', 'date', true, true], + ['phpmemory', 'integer', true, true], + ['ip', 'ip', true, true], + ['request_body', 'attachment', true, true], + ['point', 'geo_point', true, true], + ['area', 'geo_shape', true, true], + ['hashed', 'murmur3', false, true], + ['geo.coordinates', 'geo_point', true, true], + ['extension', 'text', true, true], + ['extension.keyword', 'keyword', true, true, {}, { multi: { parent: 'extension' } }], + ['machine.os', 'text', true, true], + ['machine.os.raw', 'keyword', true, true, {}, { multi: { parent: 'machine.os' } }], + ['geo.src', 'keyword', true, true], + ['_id', '_id', true, true], + ['_type', '_type', true, true], + ['_source', '_source', true, true], + ['non-filterable', 'text', true, false], + ['non-sortable', 'text', false, false], + ['custom_user_field', 'conflict', true, true], + ['script string', 'text', true, false, { script: "'i am a string'" }], + ['script number', 'long', true, false, { script: '1234' }], + ['script date', 'date', true, false, { script: '1234', lang: 'painless' }], + ['script murmur3', 'murmur3', true, false, { script: '1234' }], + ].map(function (row) { + const [name, esType, aggregatable, searchable, metadata = {}, subType = undefined] = row; + + const { + count = 0, + script, + lang = script ? 'expression' : undefined, + scripted = !!script, + } = metadata; + + // the conflict type is actually a kbnFieldType, we + // don't have any other way to represent it here + const type = esType === 'conflict' ? esType : castEsToKbnFieldTypeName(esType); + + return { + name, + type, + esTypes: [esType], + readFromDocValues: shouldReadFieldFromDocValues(aggregatable, esType), + aggregatable, + searchable, + count, + script, + lang, + scripted, + subType, + }; + }); +} + +export default stubbedLogstashFields; diff --git a/src/plugins/data/common/index_patterns/index_patterns/fixtures/stubbed_saved_object_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/fixtures/stubbed_saved_object_index_pattern.ts new file mode 100644 index 0000000000000..261e451db5452 --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/fixtures/stubbed_saved_object_index_pattern.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +// @ts-expect-error +import stubbedLogstashFields from './logstash_fields'; + +const mockLogstashFields = stubbedLogstashFields(); + +export function stubbedSavedObjectIndexPattern(id: string | null = null) { + return { + id, + type: 'index-pattern', + attributes: { + timeFieldName: 'timestamp', + customFormats: {}, + fields: mockLogstashFields, + title: 'title', + }, + version: '2', + }; +} diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 145901509d1c5..af2bbf241487c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -23,8 +23,8 @@ import { IndexPattern } from './index_pattern'; import { DuplicateField } from '../../../../kibana_utils/common'; // @ts-expect-error -import mockLogStashFields from '../../../../../fixtures/logstash_fields'; -import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; +import mockLogStashFields from './fixtures/logstash_fields'; +import { stubbedSavedObjectIndexPattern } from './fixtures/stubbed_saved_object_index_pattern'; import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index 3d32742c168ad..18f18ede86181 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -20,7 +20,7 @@ import { defaults } from 'lodash'; import { IndexPatternsService, IndexPattern } from '.'; import { fieldFormatsMock } from '../../field_formats/mocks'; -import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; +import { stubbedSavedObjectIndexPattern } from './fixtures/stubbed_saved_object_index_pattern'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; const createFieldsFetcher = jest.fn().mockImplementation(() => ({ diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 3333dba36fe69..a6c87a76f739c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -46,7 +46,6 @@ import { IndexPatternMissingIndices } from '../lib'; import { findByTitle } from '../utils'; import { DuplicateIndexPatternError } from '../errors'; -const indexPatternCache = createIndexPatternCache(); const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const savedObjectType = 'index-pattern'; @@ -72,6 +71,8 @@ export class IndexPatternsService { private fieldFormats: FieldFormatsStartCommon; private onNotification: OnNotification; private onError: OnError; + private indexPatternCache: ReturnType; + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; constructor({ @@ -93,6 +94,8 @@ export class IndexPatternsService { uiSettings, onRedirectNoIndexPattern ); + + this.indexPatternCache = createIndexPatternCache(); } /** @@ -175,9 +178,9 @@ export class IndexPatternsService { clearCache = (id?: string) => { this.savedObjectsCache = null; if (id) { - indexPatternCache.clear(id); + this.indexPatternCache.clear(id); } else { - indexPatternCache.clearAll(); + this.indexPatternCache.clearAll(); } }; @@ -435,11 +438,12 @@ export class IndexPatternsService { get = async (id: string): Promise => { const indexPatternPromise = - indexPatternCache.get(id) || indexPatternCache.set(id, this.getSavedObjectAndInit(id)); + this.indexPatternCache.get(id) || + this.indexPatternCache.set(id, this.getSavedObjectAndInit(id)); // don't cache failed requests indexPatternPromise.catch(() => { - indexPatternCache.clear(id); + this.indexPatternCache.clear(id); }); return indexPatternPromise; @@ -503,7 +507,7 @@ export class IndexPatternsService { id: indexPattern.id, }); indexPattern.id = response.id; - indexPatternCache.set(indexPattern.id, Promise.resolve(indexPattern)); + this.indexPatternCache.set(indexPattern.id, Promise.resolve(indexPattern)); return indexPattern; } @@ -586,7 +590,7 @@ export class IndexPatternsService { indexPattern.version = samePattern.version; // Clear cache - indexPatternCache.clear(indexPattern.id!); + this.indexPatternCache.clear(indexPattern.id!); // Try the save again return this.updateSavedObject(indexPattern, saveAttempts, ignoreErrors); @@ -600,7 +604,7 @@ export class IndexPatternsService { * @param indexPatternId: Id of kibana Index Pattern to delete */ async delete(indexPatternId: string) { - indexPatternCache.clear(indexPatternId); + this.indexPatternCache.clear(indexPatternId); return this.savedObjectsClient.delete('index-pattern', indexPatternId); } } diff --git a/src/plugins/data/common/search/tabify/fixtures/fake_hierarchical_data.ts b/src/plugins/data/common/search/tabify/fixtures/fake_hierarchical_data.ts new file mode 100644 index 0000000000000..4480caae39664 --- /dev/null +++ b/src/plugins/data/common/search/tabify/fixtures/fake_hierarchical_data.ts @@ -0,0 +1,632 @@ +/* + * 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 const metricOnly = { + hits: { total: 1000, hits: [], max_score: 0 }, + aggregations: { + agg_1: { value: 412032 }, + }, +}; + +export const threeTermBuckets = { + hits: { total: 1000, hits: [], max_score: 0 }, + aggregations: { + agg_2: { + buckets: [ + { + key: 'png', + doc_count: 50, + agg_1: { value: 412032 }, + agg_3: { + buckets: [ + { + key: 'IT', + doc_count: 10, + agg_1: { value: 9299 }, + agg_4: { + buckets: [ + { key: 'win', doc_count: 4, agg_1: { value: 0 } }, + { key: 'mac', doc_count: 6, agg_1: { value: 9299 } }, + ], + }, + }, + { + key: 'US', + doc_count: 20, + agg_1: { value: 8293 }, + agg_4: { + buckets: [ + { key: 'linux', doc_count: 12, agg_1: { value: 3992 } }, + { key: 'mac', doc_count: 8, agg_1: { value: 3029 } }, + ], + }, + }, + ], + }, + }, + { + key: 'css', + doc_count: 20, + agg_1: { value: 412032 }, + agg_3: { + buckets: [ + { + key: 'MX', + doc_count: 7, + agg_1: { value: 9299 }, + agg_4: { + buckets: [ + { key: 'win', doc_count: 3, agg_1: { value: 4992 } }, + { key: 'mac', doc_count: 4, agg_1: { value: 5892 } }, + ], + }, + }, + { + key: 'US', + doc_count: 13, + agg_1: { value: 8293 }, + agg_4: { + buckets: [ + { key: 'linux', doc_count: 12, agg_1: { value: 3992 } }, + { key: 'mac', doc_count: 1, agg_1: { value: 3029 } }, + ], + }, + }, + ], + }, + }, + { + key: 'html', + doc_count: 90, + agg_1: { value: 412032 }, + agg_3: { + buckets: [ + { + key: 'CN', + doc_count: 85, + agg_1: { value: 9299 }, + agg_4: { + buckets: [ + { key: 'win', doc_count: 46, agg_1: { value: 4992 } }, + { key: 'mac', doc_count: 39, agg_1: { value: 5892 } }, + ], + }, + }, + { + key: 'FR', + doc_count: 15, + agg_1: { value: 8293 }, + agg_4: { + buckets: [ + { key: 'win', doc_count: 3, agg_1: { value: 3992 } }, + { key: 'mac', doc_count: 12, agg_1: { value: 3029 } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, +}; + +export const oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { + hits: { total: 1000, hits: [], max_score: 0 }, + aggregations: { + agg_3: { + buckets: [ + { + key: 'png', + doc_count: 50, + agg_4: { + buckets: [ + { + key_as_string: '2014-09-28T00:00:00.000Z', + key: 1411862400000, + doc_count: 1, + agg_1: { value: 9283 }, + agg_2: { value: 1411862400000 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 23, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-09-29T00:00:00.000Z', + key: 1411948800000, + doc_count: 2, + agg_1: { value: 28349 }, + agg_2: { value: 1411948800000 }, + agg_5: { value: 203 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 39, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-09-30T00:00:00.000Z', + key: 1412035200000, + doc_count: 3, + agg_1: { value: 84330 }, + agg_2: { value: 1412035200000 }, + agg_5: { value: 200 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 329, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-01T00:00:00.000Z', + key: 1412121600000, + doc_count: 4, + agg_1: { value: 34992 }, + agg_2: { value: 1412121600000 }, + agg_5: { value: 103 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 22, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-02T00:00:00.000Z', + key: 1412208000000, + doc_count: 5, + agg_1: { value: 145432 }, + agg_2: { value: 1412208000000 }, + agg_5: { value: 153 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 93, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-03T00:00:00.000Z', + key: 1412294400000, + doc_count: 35, + agg_1: { value: 220943 }, + agg_2: { value: 1412294400000 }, + agg_5: { value: 239 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 72, + }, + }, + ], + }, + }, + }, + ], + }, + }, + { + key: 'css', + doc_count: 20, + agg_4: { + buckets: [ + { + key_as_string: '2014-09-28T00:00:00.000Z', + key: 1411862400000, + doc_count: 1, + agg_1: { value: 9283 }, + agg_2: { value: 1411862400000 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 75, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-09-29T00:00:00.000Z', + key: 1411948800000, + doc_count: 2, + agg_1: { value: 28349 }, + agg_2: { value: 1411948800000 }, + agg_5: { value: 10 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 11, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-09-30T00:00:00.000Z', + key: 1412035200000, + doc_count: 3, + agg_1: { value: 84330 }, + agg_2: { value: 1412035200000 }, + agg_5: { value: 24 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 238, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-01T00:00:00.000Z', + key: 1412121600000, + doc_count: 4, + agg_1: { value: 34992 }, + agg_2: { value: 1412121600000 }, + agg_5: { value: 49 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 343, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-02T00:00:00.000Z', + key: 1412208000000, + doc_count: 5, + agg_1: { value: 145432 }, + agg_2: { value: 1412208000000 }, + agg_5: { value: 100 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 837, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-03T00:00:00.000Z', + key: 1412294400000, + doc_count: 5, + agg_1: { value: 220943 }, + agg_2: { value: 1412294400000 }, + agg_5: { value: 23 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 302, + }, + }, + ], + }, + }, + }, + ], + }, + }, + { + key: 'html', + doc_count: 90, + agg_4: { + buckets: [ + { + key_as_string: '2014-09-28T00:00:00.000Z', + key: 1411862400000, + doc_count: 10, + agg_1: { value: 9283 }, + agg_2: { value: 1411862400000 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 30, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-09-29T00:00:00.000Z', + key: 1411948800000, + doc_count: 20, + agg_1: { value: 28349 }, + agg_2: { value: 1411948800000 }, + agg_5: { value: 1 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 43, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-09-30T00:00:00.000Z', + key: 1412035200000, + doc_count: 30, + agg_1: { value: 84330 }, + agg_2: { value: 1412035200000 }, + agg_5: { value: 5 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 88, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-01T00:00:00.000Z', + key: 1412121600000, + doc_count: 11, + agg_1: { value: 34992 }, + agg_2: { value: 1412121600000 }, + agg_5: { value: 10 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 91, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-02T00:00:00.000Z', + key: 1412208000000, + doc_count: 12, + agg_1: { value: 145432 }, + agg_2: { value: 1412208000000 }, + agg_5: { value: 43 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 534, + }, + }, + ], + }, + }, + }, + { + key_as_string: '2014-10-03T00:00:00.000Z', + key: 1412294400000, + doc_count: 7, + agg_1: { value: 220943 }, + agg_2: { value: 1412294400000 }, + agg_5: { value: 1 }, + agg_6: { + hits: { + total: 2, + hits: [ + { + fields: { + bytes: 553, + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, +}; + +export const oneRangeBucket = { + took: 35, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + }, + hits: { + total: 6039, + max_score: 0, + hits: [], + }, + aggregations: { + agg_2: { + buckets: { + '0.0-1000.0': { + from: 0, + from_as_string: '0.0', + to: 1000, + to_as_string: '1000.0', + doc_count: 606, + }, + '1000.0-2000.0': { + from: 1000, + from_as_string: '1000.0', + to: 2000, + to_as_string: '2000.0', + doc_count: 298, + }, + }, + }, + }, +}; + +export const oneFilterBucket = { + took: 11, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + }, + hits: { + total: 6005, + max_score: 0, + hits: [], + }, + aggregations: { + agg_2: { + buckets: { + 'type:apache': { + doc_count: 4844, + }, + 'type:nginx': { + doc_count: 1161, + }, + }, + }, + }, +}; + +export const oneHistogramBucket = { + took: 37, + timed_out: false, + _shards: { + total: 6, + successful: 6, + failed: 0, + }, + hits: { + total: 49208, + max_score: 0, + hits: [], + }, + aggregations: { + agg_2: { + buckets: [ + { + key_as_string: '2014-09-28T00:00:00.000Z', + key: 1411862400000, + doc_count: 8247, + }, + { + key_as_string: '2014-09-29T00:00:00.000Z', + key: 1411948800000, + doc_count: 8184, + }, + { + key_as_string: '2014-09-30T00:00:00.000Z', + key: 1412035200000, + doc_count: 8269, + }, + { + key_as_string: '2014-10-01T00:00:00.000Z', + key: 1412121600000, + doc_count: 8141, + }, + { + key_as_string: '2014-10-02T00:00:00.000Z', + key: 1412208000000, + doc_count: 8148, + }, + { + key_as_string: '2014-10-03T00:00:00.000Z', + key: 1412294400000, + doc_count: 8219, + }, + ], + }, + }, +}; diff --git a/src/plugins/data/common/search/tabify/tabify.test.ts b/src/plugins/data/common/search/tabify/tabify.test.ts index 6b9d520b11436..1ee5d23230396 100644 --- a/src/plugins/data/common/search/tabify/tabify.test.ts +++ b/src/plugins/data/common/search/tabify/tabify.test.ts @@ -21,7 +21,7 @@ import { tabifyAggResponse } from './tabify'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; import { AggConfigs, IAggConfig, IAggConfigs } from '../aggs'; import { mockAggTypesRegistry } from '../aggs/test_helpers'; -import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; +import { metricOnly, threeTermBuckets } from './fixtures/fake_hierarchical_data'; describe('tabifyAggResponse Integration', () => { const typesRegistry = mockAggTypesRegistry(); diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 695ee34d3b468..34e411aa85c80 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -29,10 +29,22 @@ export type ISearchGeneric = < ) => Observable; export type ISearchCancelGeneric = (id: string, options?: ISearchOptions) => Promise; +export type ISearchExtendGeneric = ( + id: string, + keepAlive: string, + options?: ISearchOptions +) => Promise; export interface ISearchClient { search: ISearchGeneric; + /** + * Used to cancel an in-progress search request. + */ cancel: ISearchCancelGeneric; + /** + * Used to extend the TTL of an in-progress search request. + */ + extend: ISearchExtendGeneric; } export interface IKibanaSearchResponse { diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index 84ce5b0382624..909d6e97c221c 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../kibana_react/public'; -import { ActionByType, createAction, IncompatibleActionError } from '../../../ui_actions/public'; +import { Action, createAction, IncompatibleActionError } from '../../../ui_actions/public'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; import { Filter, FilterManager, TimefilterContract, esFilters } from '..'; @@ -41,8 +41,8 @@ async function isCompatible(context: ApplyGlobalFilterActionContext) { export function createFilterAction( filterManager: FilterManager, timeFilter: TimefilterContract -): ActionByType { - return createAction({ +): Action { + return createAction({ type: ACTION_GLOBAL_APPLY_FILTER, id: ACTION_GLOBAL_APPLY_FILTER, order: 100, diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts index 6dcfa4d02bcb2..9830bf5b7ba83 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -29,12 +29,16 @@ import { } from '../../../public'; import { dataPluginMock } from '../../../public/mocks'; import { setIndexPatterns, setSearchService } from '../../../public/services'; -import { TriggerContextMapping } from '../../../../ui_actions/public'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; - let baseEvent: TriggerContextMapping['SELECT_RANGE_TRIGGER']['data']; + let baseEvent: { + table: any; + column: number; + range: number[]; + timeFieldName?: string; + }; const mockField = { name: 'time', diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 3b84523d782f6..c6d234665c05e 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -18,7 +18,7 @@ */ import { Datatable } from 'src/plugins/expressions/public'; -import { ActionByType, createAction, UiActionsStart } from '../../../../plugins/ui_actions/public'; +import { Action, createAction, UiActionsStart } from '../../../../plugins/ui_actions/public'; import { APPLY_FILTER_TRIGGER } from '../triggers'; import { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; @@ -38,8 +38,8 @@ export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; export function createSelectRangeAction( getStartServices: () => { uiActions: UiActionsStart } -): ActionByType { - return createAction({ +): Action { + return createAction({ type: ACTION_SELECT_RANGE, id: ACTION_SELECT_RANGE, shouldAutoExecute: async () => true, diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 8f207e94e8fbe..41c2943a6a5bb 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -18,7 +18,7 @@ */ import { Datatable } from 'src/plugins/expressions/public'; -import { ActionByType, createAction, UiActionsStart } from '../../../../plugins/ui_actions/public'; +import { Action, createAction, UiActionsStart } from '../../../../plugins/ui_actions/public'; import { APPLY_FILTER_TRIGGER } from '../triggers'; import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; import type { Filter } from '../../common/es_query/filters'; @@ -44,8 +44,8 @@ export interface ValueClickContext { export function createValueClickAction( getStartServices: () => { uiActions: UiActionsStart } -): ActionByType { - return createAction({ +): Action { + return createAction({ type: ACTION_VALUE_CLICK, id: ACTION_VALUE_CLICK, shouldAutoExecute: async () => true, diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 7b15e2576e704..bcb65aa0ee205 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -385,7 +385,7 @@ export { SearchRequest, SearchSourceFields, SortDirection, - SessionState, + SearchSessionState, // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index c60a1efabf987..43abe84950fdb 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -53,11 +53,6 @@ import { createFilterAction, createFiltersFromValueClickAction, createFiltersFromRangeSelectAction, - ApplyGlobalFilterActionContext, - ACTION_SELECT_RANGE, - ACTION_VALUE_CLICK, - SelectRangeActionContext, - ValueClickActionContext, createValueClickAction, createSelectRangeAction, } from './actions'; @@ -66,19 +61,6 @@ import { SavedObjectsClientPublicToCommon } from './index_patterns'; import { getIndexPatternLoad } from './index_patterns/expressions'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { getTableViewDescription } from './utils/table_inspector_view'; -import { TriggerId } from '../../ui_actions/public'; - -declare module '../../ui_actions/public' { - export interface TriggerContextMapping { - [APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext; - } - - export interface ActionContextMapping { - [ACTION_GLOBAL_APPLY_FILTER]: ApplyGlobalFilterActionContext; - [ACTION_SELECT_RANGE]: SelectRangeActionContext; - [ACTION_VALUE_CLICK]: ValueClickActionContext; - } -} export class DataPublicPlugin implements @@ -125,14 +107,14 @@ export class DataPublicPlugin ); uiActions.addTriggerAction( - 'SELECT_RANGE_TRIGGER' as TriggerId, + 'SELECT_RANGE_TRIGGER', createSelectRangeAction(() => ({ uiActions: startServices().plugins.uiActions, })) ); uiActions.addTriggerAction( - 'VALUE_CLICK_TRIGGER' as TriggerId, + 'VALUE_CLICK_TRIGGER', createValueClickAction(() => ({ uiActions: startServices().plugins.uiActions, })) diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 3493844a71ac1..27a40b4e5ffcb 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2311,6 +2311,17 @@ export interface SearchSessionInfoProvider; } +// @public +export enum SearchSessionState { + BackgroundCompleted = "backgroundCompleted", + BackgroundLoading = "backgroundLoading", + Canceled = "canceled", + Completed = "completed", + Loading = "loading", + None = "none", + Restored = "restored" +} + // @public (undocumented) export class SearchSource { // Warning: (ae-forgotten-export) The symbol "SearchSourceDependencies" needs to be exported by the entry point index.d.ts @@ -2418,17 +2429,6 @@ export class SearchTimeoutError extends KbnError { mode: TimeoutErrorMode; } -// @public -export enum SessionState { - BackgroundCompleted = "backgroundCompleted", - BackgroundLoading = "backgroundLoading", - Canceled = "canceled", - Completed = "completed", - Loading = "loading", - None = "none", - Restored = "restored" -} - // Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2620,7 +2620,7 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:46:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:50:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 2a767d1bf7c0d..47aedd49d66e9 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -45,7 +45,7 @@ export { SessionService, ISessionService, SearchSessionInfoProvider, - SessionState, + SearchSessionState, SessionsClient, ISessionsClient, } from './session'; diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts index ee0121aacad5e..a40b8857fc0e0 100644 --- a/src/plugins/data/public/search/session/index.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -18,5 +18,5 @@ */ export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service'; -export { SessionState } from './session_state'; +export { SearchSessionState } from './search_session_state'; export { SessionsClient, ISessionsClient } from './sessions_client'; diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index 0ff99b3080365..ea0cd8be03f27 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -20,7 +20,7 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { ISessionsClient } from './sessions_client'; import { ISessionService } from './session_service'; -import { SessionState } from './session_state'; +import { SearchSessionState } from './search_session_state'; export function getSessionsClientMock(): jest.Mocked { return { @@ -39,7 +39,7 @@ export function getSessionServiceMock(): jest.Mocked { restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), - state$: new BehaviorSubject(SessionState.None).asObservable(), + state$: new BehaviorSubject(SearchSessionState.None).asObservable(), setSearchSessionInfoProvider: jest.fn(), trackSearch: jest.fn((searchDescriptor) => () => {}), destroy: jest.fn(), diff --git a/src/plugins/data/public/search/session/session_state.test.ts b/src/plugins/data/public/search/session/search_session_state.test.ts similarity index 61% rename from src/plugins/data/public/search/session/session_state.test.ts rename to src/plugins/data/public/search/session/search_session_state.test.ts index 5f709c75bb5d2..539fc8252b2a5 100644 --- a/src/plugins/data/public/search/session/session_state.test.ts +++ b/src/plugins/data/public/search/session/search_session_state.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createSessionStateContainer, SessionState } from './session_state'; +import { createSessionStateContainer, SearchSessionState } from './search_session_state'; describe('Session state container', () => { const { stateContainer: state } = createSessionStateContainer(); @@ -29,7 +29,7 @@ describe('Session state container', () => { describe('transitions', () => { test('start', () => { state.transitions.start(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).not.toBeUndefined(); }); @@ -39,22 +39,22 @@ describe('Session state container', () => { state.transitions.start(); state.transitions.trackSearch({}); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); }); test('untrack', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.unTrackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Completed); + expect(state.selectors.getState()).toBe(SearchSessionState.Completed); }); test('clear', () => { state.transitions.start(); state.transitions.clear(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).toBeUndefined(); }); @@ -64,11 +64,11 @@ describe('Session state container', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.cancel(); - expect(state.selectors.getState()).toBe(SessionState.Canceled); + expect(state.selectors.getState()).toBe(SearchSessionState.Canceled); state.transitions.clear(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); test('store -> completed', () => { @@ -77,48 +77,48 @@ describe('Session state container', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.store(); - expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.unTrackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.BackgroundCompleted); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundCompleted); state.transitions.clear(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); test('store -> cancel', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.store(); - expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.cancel(); - expect(state.selectors.getState()).toBe(SessionState.Canceled); + expect(state.selectors.getState()).toBe(SearchSessionState.Canceled); state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Canceled); + expect(state.selectors.getState()).toBe(SearchSessionState.Canceled); state.transitions.start(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); test('restore', () => { const id = 'id'; state.transitions.restore(id); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.unTrackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(state.selectors.getState()).toBe(SearchSessionState.Restored); expect(() => state.transitions.store()).toThrowError(); - expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(state.selectors.getState()).toBe(SearchSessionState.Restored); expect(() => state.transitions.cancel()).toThrowError(); - expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(state.selectors.getState()).toBe(SearchSessionState.Restored); state.transitions.start(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); }); }); diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/search_session_state.ts similarity index 86% rename from src/plugins/data/public/search/session/session_state.ts rename to src/plugins/data/public/search/session/search_session_state.ts index 087417263e5bf..7a35a65a600d7 100644 --- a/src/plugins/data/public/search/session/session_state.ts +++ b/src/plugins/data/public/search/session/search_session_state.ts @@ -27,7 +27,7 @@ import { createStateContainer, StateContainer } from '../../../../kibana_utils/p * * @public */ -export enum SessionState { +export enum SearchSessionState { /** * Session is not active, e.g. didn't start */ @@ -39,18 +39,18 @@ export enum SessionState { Loading = 'loading', /** - * No action was taken and the page completed loading without background session creation. + * No action was taken and the page completed loading without search session creation. */ Completed = 'completed', /** - * Search request was sent to the background. + * Search session was sent to the background. * The page is loading in background. */ BackgroundLoading = 'backgroundLoading', /** - * Page load completed with background session created. + * Page load completed with search session created. */ BackgroundCompleted = 'backgroundCompleted', @@ -68,7 +68,7 @@ export enum SessionState { /** * Internal state of SessionService - * {@link SessionState} is inferred from this state + * {@link SearchSessionState} is inferred from this state * * @private */ @@ -179,27 +179,29 @@ export interface SessionPureSelectors< SearchDescriptor = unknown, S = SessionStateInternal > { - getState: (state: S) => () => SessionState; + getState: (state: S) => () => SearchSessionState; } export const sessionPureSelectors: SessionPureSelectors = { getState: (state) => () => { - if (!state.sessionId) return SessionState.None; - if (!state.isStarted) return SessionState.None; - if (state.isCanceled) return SessionState.Canceled; + if (!state.sessionId) return SearchSessionState.None; + if (!state.isStarted) return SearchSessionState.None; + if (state.isCanceled) return SearchSessionState.Canceled; switch (true) { case state.isRestore: return state.pendingSearches.length > 0 - ? SessionState.BackgroundLoading - : SessionState.Restored; + ? SearchSessionState.BackgroundLoading + : SearchSessionState.Restored; case state.isStored: return state.pendingSearches.length > 0 - ? SessionState.BackgroundLoading - : SessionState.BackgroundCompleted; + ? SearchSessionState.BackgroundLoading + : SearchSessionState.BackgroundCompleted; default: - return state.pendingSearches.length > 0 ? SessionState.Loading : SessionState.Completed; + return state.pendingSearches.length > 0 + ? SearchSessionState.Loading + : SearchSessionState.Completed; } - return SessionState.None; + return SearchSessionState.None; }, }; @@ -213,7 +215,7 @@ export const createSessionStateContainer = ( { freeze = true }: { freeze: boolean } = { freeze: true } ): { stateContainer: SessionStateContainer; - sessionState$: Observable; + sessionState$: Observable; } => { const stateContainer = createStateContainer( createSessionDefaultState(), @@ -222,7 +224,7 @@ export const createSessionStateContainer = ( freeze ? undefined : { freeze: (s) => s } ) as SessionStateContainer; - const sessionState$: Observable = stateContainer.state$.pipe( + const sessionState$: Observable = stateContainer.state$.pipe( map(() => stateContainer.selectors.getState()), distinctUntilChanged(), shareReplay(1) diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 83c3185ead63e..cf083239b1571 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -22,11 +22,11 @@ import { coreMock } from '../../../../../core/public/mocks'; import { take, toArray } from 'rxjs/operators'; import { getSessionsClientMock } from './mocks'; import { BehaviorSubject } from 'rxjs'; -import { SessionState } from './session_state'; +import { SearchSessionState } from './search_session_state'; describe('Session service', () => { let sessionService: ISessionService; - let state$: BehaviorSubject; + let state$: BehaviorSubject; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); @@ -36,7 +36,7 @@ describe('Session service', () => { getSessionsClientMock(), { freezeState: false } // needed to use mocks inside state container ); - state$ = new BehaviorSubject(SessionState.None); + state$ = new BehaviorSubject(SearchSessionState.None); sessionService.state$.subscribe(state$); }); @@ -65,17 +65,17 @@ describe('Session service', () => { it('Tracks searches for current session', () => { expect(() => sessionService.trackSearch({ abort: () => {} })).toThrowError(); - expect(state$.getValue()).toBe(SessionState.None); + expect(state$.getValue()).toBe(SearchSessionState.None); sessionService.start(); const untrack1 = sessionService.trackSearch({ abort: () => {} }); - expect(state$.getValue()).toBe(SessionState.Loading); + expect(state$.getValue()).toBe(SearchSessionState.Loading); const untrack2 = sessionService.trackSearch({ abort: () => {} }); - expect(state$.getValue()).toBe(SessionState.Loading); + expect(state$.getValue()).toBe(SearchSessionState.Loading); untrack1(); - expect(state$.getValue()).toBe(SessionState.Loading); + expect(state$.getValue()).toBe(SearchSessionState.Loading); untrack2(); - expect(state$.getValue()).toBe(SessionState.Completed); + expect(state$.getValue()).toBe(SearchSessionState.Completed); }); it('Cancels all tracked searches within current session', async () => { diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index ef0b36a33be52..2bbb762fcfe9f 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -23,7 +23,11 @@ import { Observable, Subject, Subscription } from 'rxjs'; import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; import { UrlGeneratorId, UrlGeneratorStateMapping } from '../../../../share/public/'; import { ConfigSchema } from '../../../config'; -import { createSessionStateContainer, SessionState, SessionStateContainer } from './session_state'; +import { + createSessionStateContainer, + SearchSessionState, + SessionStateContainer, +} from './search_session_state'; import { ISessionsClient } from './sessions_client'; export type ISessionService = PublicContract; @@ -33,12 +37,12 @@ export interface TrackSearchDescriptor { } /** - * Provide info about current search session to be stored in backgroundSearch saved object + * Provide info about current search session to be stored in the Search Session saved object */ export interface SearchSessionInfoProvider { /** * User-facing name of the session. - * e.g. will be displayed in background sessions management list + * e.g. will be displayed in saved Search Sessions management list */ getName: () => Promise; getUrlGeneratorData: () => Promise<{ @@ -52,7 +56,7 @@ export interface SearchSessionInfoProvider; + public readonly state$: Observable; private readonly state: SessionStateContainer; private searchSessionInfoProvider?: SearchSessionInfoProvider; @@ -95,7 +99,7 @@ export class SessionService { /** * Set a provider of info about current session - * This will be used for creating a background session saved object + * This will be used for creating a search session saved object * @param searchSessionInfoProvider */ public setSearchSessionInfoProvider( @@ -184,7 +188,7 @@ export class SessionService { private refresh$ = new Subject(); /** * Observable emits when search result refresh was requested - * For example, search to background UI could have it's own "refresh" button + * For example, the UI could have it's own "refresh" button * Application would use this observable to handle user interaction on that button */ public onRefresh$ = this.refresh$.asObservable(); diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index 38be647a37c7a..a8031e4e467e7 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -27,7 +27,7 @@ export interface SessionsClientDeps { } /** - * CRUD backgroundSession SO + * CRUD Search Session SO */ export class SessionsClient { private readonly http: HttpSetup; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 057b242c22f20..7b0b501af8169 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -45,7 +45,7 @@ export interface ISearchSetup { */ session: ISessionService; /** - * Background search sessions SO CRUD + * Search sessions SO CRUD * {@link ISessionsClient} */ sessionsClient: ISessionsClient; @@ -84,7 +84,7 @@ export interface ISearchStart { */ session: ISessionService; /** - * Background search sessions SO CRUD + * Search sessions SO CRUD * {@link ISessionsClient} */ sessionsClient: ISessionsClient; diff --git a/src/plugins/data/public/triggers/apply_filter_trigger.ts b/src/plugins/data/public/triggers/apply_filter_trigger.ts index 816c1737608da..07422eb313ae3 100644 --- a/src/plugins/data/public/triggers/apply_filter_trigger.ts +++ b/src/plugins/data/public/triggers/apply_filter_trigger.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { Trigger } from '../../../ui_actions/public'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; -export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { +export const applyFilterTrigger: Trigger = { id: APPLY_FILTER_TRIGGER, title: i18n.translate('data.triggers.applyFilterTitle', { defaultMessage: 'Apply filter', diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 5e6fd5323c0b7..0d730aed0b28a 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -71,19 +71,27 @@ export function FilterItem(props: Props) { useEffect(() => { const index = props.filter.meta.index; + let isSubscribed = true; if (index) { getIndexPatterns() .get(index) .then((indexPattern) => { - setIndexPatternExists(!!indexPattern); + if (isSubscribed) { + setIndexPatternExists(!!indexPattern); + } }) .catch(() => { - setIndexPatternExists(false); + if (isSubscribed) { + setIndexPatternExists(false); + } }); - } else { + } else if (isSubscribed) { // Allow filters without an index pattern and don't validate them. setIndexPatternExists(true); } + return () => { + isSubscribed = false; + }; }, [props.filter.meta.index]); function handleBadgeClick(e: MouseEvent) { @@ -145,7 +153,14 @@ export function FilterItem(props: Props) { const dataTestSubjNegated = filter.meta.negate ? 'filter-negated' : ''; const dataTestSubjDisabled = `filter-${isDisabled(labelConfig) ? 'disabled' : 'enabled'}`; const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; - return `filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue} ${dataTestSubjPinned} ${dataTestSubjNegated}`; + return classNames( + 'filter', + dataTestSubjDisabled, + dataTestSubjKey, + dataTestSubjValue, + dataTestSubjPinned, + dataTestSubjNegated + ); } function getPanels() { diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx index f842568859fc2..f4d1a8988da78 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx @@ -38,7 +38,7 @@ import { DataViewRow, DataViewColumn } from '../types'; import { IUiSettingsClient } from '../../../../../../core/public'; import { Datatable, DatatableColumn } from '../../../../../expressions/public'; import { FieldFormatsStart } from '../../../field_formats'; -import { TriggerId, UiActionsStart } from '../../../../../ui_actions/public'; +import { UiActionsStart } from '../../../../../ui_actions/public'; interface DataTableFormatState { columns: DataViewColumn[]; @@ -112,7 +112,7 @@ export class DataTableFormat extends Component { const value = table.rows[rowIndex][column.id]; const eventData = { table, column: columnIndex, row: rowIndex, value }; - uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER' as TriggerId, { + uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER', { data: { data: [eventData] }, }); }} @@ -145,7 +145,7 @@ export class DataTableFormat extends Component { const value = table.rows[rowIndex][column.id]; const eventData = { table, column: columnIndex, row: rowIndex, value }; - uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER' as TriggerId, { + uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER', { data: { data: [eventData], negate: true }, }); }} diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_view.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_view.tsx index 97dca45d742c9..1fae5e221f3ad 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/data_view.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_view.tsx @@ -38,6 +38,7 @@ interface DataViewComponentProps extends InspectorViewProps { uiActions: UiActionsStart; fieldFormats: FieldFormatsStart; isFilterable: (column: DatatableColumn) => boolean; + options: { fileName?: string }; } class DataViewComponent extends Component { @@ -122,7 +123,7 @@ class DataViewComponent extends Component ); }; diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts index 1794df7391cb0..038f340babb1f 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts @@ -18,7 +18,7 @@ */ import { fetchProvider } from './fetch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; @@ -30,7 +30,7 @@ jest.mock('../../../common', () => ({ })); let fetch: ReturnType; -let callCluster: LegacyAPICaller; +let esClient: ElasticsearchClient; let collectorFetchContext: CollectorFetchContext; const collectorFetchContextMock = createCollectorFetchContextMock(); @@ -38,34 +38,33 @@ function setupMockCallCluster( optCount: { optInCount?: number; optOutCount?: number } | null, language: string | undefined | null ) { - callCluster = (jest.fn((method, params) => { - if (params && 'id' in params && params.id === 'kql-telemetry:kql-telemetry') { - if (optCount === null) { - return Promise.resolve({ + function mockedEsGetMethod() { + if (optCount === null) { + return Promise.resolve({ + body: { _index: '.kibana_1', _id: 'kql-telemetry:kql-telemetry', found: false, - }); - } else { - return Promise.resolve({ + }, + }); + } else { + return Promise.resolve({ + body: { _source: { - 'kql-telemetry': { - ...optCount, - }, + 'kql-telemetry': { ...optCount }, type: 'kql-telemetry', updated_at: '2018-10-05T20:20:56.258Z', }, - }); - } - } else if (params && 'body' in params && params.body.query.term.type === 'config') { - if (language === 'missingConfigDoc') { - return Promise.resolve({ - hits: { - hits: [], - }, - }); - } else { - return Promise.resolve({ + }, + }); + } + } + function mockedEsSearchMethod() { + if (language === 'missingConfigDoc') { + return Promise.resolve({ body: { hits: { hits: [] } } }); + } else { + return Promise.resolve({ + body: { hits: { hits: [ { @@ -77,12 +76,15 @@ function setupMockCallCluster( }, ], }, - }); - } + }, + }); } - - throw new Error('invalid call'); - }) as unknown) as LegacyAPICaller; + } + const esClientMock = ({ + get: jest.fn().mockImplementation(mockedEsGetMethod), + search: jest.fn().mockImplementation(mockedEsSearchMethod), + } as unknown) as ElasticsearchClient; + esClient = esClientMock; } describe('makeKQLUsageCollector', () => { @@ -95,7 +97,7 @@ describe('makeKQLUsageCollector', () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); collectorFetchContext = { ...collectorFetchContextMock, - callCluster, + esClient, }; const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(1); @@ -106,7 +108,7 @@ describe('makeKQLUsageCollector', () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); collectorFetchContext = { ...collectorFetchContextMock, - callCluster, + esClient, }; const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('kuery'); @@ -117,7 +119,7 @@ describe('makeKQLUsageCollector', () => { setupMockCallCluster({ optInCount: 1 }, null); collectorFetchContext = { ...collectorFetchContextMock, - callCluster, + esClient, }; const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('lucene'); @@ -127,7 +129,7 @@ describe('makeKQLUsageCollector', () => { setupMockCallCluster({ optInCount: 1 }, undefined); collectorFetchContext = { ...collectorFetchContextMock, - callCluster, + esClient, }; const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); @@ -137,7 +139,7 @@ describe('makeKQLUsageCollector', () => { setupMockCallCluster(null, 'kuery'); collectorFetchContext = { ...collectorFetchContextMock, - callCluster, + esClient, }; const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(0); @@ -148,7 +150,7 @@ describe('makeKQLUsageCollector', () => { setupMockCallCluster(null, 'missingConfigDoc'); collectorFetchContext = { ...collectorFetchContextMock, - callCluster, + esClient, }; const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts index 21a1843d1ec81..5178aa65705d8 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts @@ -30,18 +30,22 @@ export interface Usage { } export function fetchProvider(index: string) { - return async ({ callCluster }: CollectorFetchContext): Promise => { - const [response, config] = await Promise.all([ - callCluster('get', { - index, - id: 'kql-telemetry:kql-telemetry', - ignore: [404], - }), - callCluster('search', { - index, - body: { query: { term: { type: 'config' } } }, - ignore: [404], - }), + return async ({ esClient }: CollectorFetchContext): Promise => { + const [{ body: response }, { body: config }] = await Promise.all([ + esClient.get( + { + index, + id: 'kql-telemetry:kql-telemetry', + }, + { ignore: [404] } + ), + esClient.search( + { + index, + body: { query: { term: { type: 'config' } } }, + }, + { ignore: [404] } + ), ]); const queryLanguageConfigValue: string | null | undefined = get( diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts index 344bc18c7b4b6..9d0d431cf4eaf 100644 --- a/src/plugins/data/server/search/collectors/fetch.ts +++ b/src/plugins/data/server/search/collectors/fetch.ts @@ -20,31 +20,34 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { SharedGlobalConfig } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { Usage } from './register'; - -interface SearchTelemetrySavedObject { +interface SearchTelemetry { 'search-telemetry': Usage; } +type ESResponse = SearchResponse; export function fetchProvider(config$: Observable) { - return async ({ callCluster }: CollectorFetchContext): Promise => { + return async ({ esClient }: CollectorFetchContext): Promise => { const config = await config$.pipe(first()).toPromise(); - - const response = await callCluster('search', { - index: config.kibana.index, - body: { - query: { term: { type: { value: 'search-telemetry' } } }, + const { body: esResponse } = await esClient.search( + { + index: config.kibana.index, + body: { + query: { term: { type: { value: 'search-telemetry' } } }, + }, }, - ignore: [404], - }); - - return response.hits.hits.length - ? response.hits.hits[0]._source['search-telemetry'] - : { - successCount: 0, - errorCount: 0, - averageDuration: null, - }; + { ignore: [404] } + ); + const size = esResponse?.hits?.hits?.length ?? 0; + if (!size) { + return { + successCount: 0, + errorCount: 0, + averageDuration: null, + }; + } + return esResponse.hits.hits[0]._source['search-telemetry']; }; } diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 5de019cd1b83e..d26c099b23f39 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -315,6 +315,19 @@ export class SearchService implements Plugin { return strategy.cancel(id, options, deps); }; + private extend = ( + id: string, + keepAlive: string, + options: ISearchOptions, + deps: SearchStrategyDependencies + ) => { + const strategy = this.getSearchStrategy(options.strategy); + if (!strategy.extend) { + throw new KbnServerError(`Search strategy ${options.strategy} does not support extend`, 400); + } + return strategy.extend(id, keepAlive, options, deps); + }; + private getSearchStrategy = < SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse @@ -344,6 +357,7 @@ export class SearchService implements Plugin { search: (searchRequest, options = {}) => this.search(scopedSession, searchRequest, options, deps), cancel: (id, options = {}) => this.cancel(id, options, deps), + extend: (id, keepAlive, options = {}) => this.extend(id, keepAlive, options, deps), }; }; }; diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index 15021436d8821..37484185cb779 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -23,7 +23,7 @@ import { ISearchStrategy } from '../types'; import { ISessionService } from './types'; /** - * The OSS session service. See data_enhanced in X-Pack for the background session service. + * The OSS session service. See data_enhanced in X-Pack for the search session service. */ export class SessionService implements ISessionService { constructor() {} diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index db8b8ac72d0e5..fb00f86464e4e 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -86,6 +86,12 @@ export interface ISearchStrategy< deps: SearchStrategyDependencies ) => Observable; cancel?: (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; + extend?: ( + id: string, + keepAlive: string, + options: ISearchOptions, + deps: SearchStrategyDependencies + ) => Promise; } export interface ISearchStart< diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index cd3527d5ad7ab..c58cd11bbc5bc 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -38,7 +38,6 @@ import { IUiSettingsClient } from 'src/core/server'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server'; import { KibanaRequest } from 'kibana/server'; import { KibanaRequest as KibanaRequest_2 } from 'src/core/server'; -import { LegacyAPICaller } from 'src/core/server'; import { Logger } from 'src/core/server'; import { Logger as Logger_2 } from 'kibana/server'; import { LoggerFactory } from '@kbn/logging'; @@ -952,6 +951,8 @@ export interface ISearchStrategy Promise; // (undocumented) + extend?: (id: string, keepAlive: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; + // (undocumented) search: (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable; } @@ -1430,7 +1431,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:279:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:70:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:90:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:106:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:112:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json new file mode 100644 index 0000000000000..81bcb3b02e100 --- /dev/null +++ b/src/plugins/data/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../bfetch/tsconfig.json" }, + { "path": "../ui_actions/tsconfig.json" }, + { "path": "../share/tsconfig.json" }, + { "path": "../inspector/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + ] +} diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 4334af63539e3..321a102e8d782 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -27,4 +27,5 @@ export const FIELDS_LIMIT_SETTING = 'fields:popularLimit'; export const CONTEXT_DEFAULT_SIZE_SETTING = 'context:defaultSize'; export const CONTEXT_STEP_SETTING = 'context:step'; export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; +export const DOC_TABLE_LEGACY = 'doc_table:legacy'; export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index 706118cb71350..f2c12315d4b90 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -22,29 +22,40 @@ import { IndexPattern } from '../../../data/common'; import { indexPatterns } from '../../../data/public'; const fields = [ + { + name: '_source', + type: '_source', + scripted: false, + filterable: false, + aggregatable: false, + }, { name: '_index', type: 'string', scripted: false, filterable: true, + aggregatable: false, }, { name: 'message', type: 'string', scripted: false, filterable: false, + aggregatable: false, }, { name: 'extension', type: 'string', scripted: false, filterable: true, + aggregatable: true, }, { name: 'bytes', type: 'number', scripted: false, filterable: true, + aggregatable: true, }, { name: 'scripted', @@ -62,16 +73,21 @@ const indexPattern = ({ id: 'the-index-pattern-id', title: 'the-index-pattern-title', metaFields: ['_index', '_score'], + formatField: jest.fn(), flattenHit: undefined, formatHit: jest.fn((hit) => hit._source), fields, - getComputedFields: () => ({}), + getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }), getSourceFiltering: () => ({}), getFieldByName: () => ({}), timeFieldName: '', + docvalueFields: [], } as unknown) as IndexPattern; indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; +indexPattern.formatField = (hit: Record, fieldName: string) => { + return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; +}; export const indexPatternMock = indexPattern; diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index d20b1ca999af9..8dc3e5c87e504 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -1,15 +1,3 @@ - - - diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index 145d3afe23224..d9e2452eb8bd6 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -56,13 +56,14 @@ getAngularModule().directive('contextApp', function ContextApp() { }); function ContextAppController($scope, Private) { - const { filterManager, indexPatterns, uiSettings } = getServices(); + const { filterManager, indexPatterns, uiSettings, navigation } = getServices(); const queryParameterActions = getQueryParameterActions(filterManager, indexPatterns); const queryActions = Private(QueryActionsProvider); this.state = createInitialState( parseInt(uiSettings.get(CONTEXT_STEP_SETTING), 10), getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)) ); + this.topNavMenu = navigation.ui.TopNavMenu; this.actions = _.mapValues( { diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 99497d61c716e..4c5cb864b5111 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -24,7 +24,6 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; - import { RequestAdapter } from '../../../../inspector/public'; import { connectToQueryState, @@ -35,6 +34,7 @@ import { import { getSortArray } from './doc_table'; import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; +import indexTemplateGrid from './discover_datagrid.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; import { @@ -124,7 +124,9 @@ app.config(($routeProvider) => { }; const discoverRoute = { ...defaults, - template: indexTemplateLegacy, + template: getServices().uiSettings.get('doc_table:legacy', true) + ? indexTemplateLegacy + : indexTemplateGrid, reloadOnSearch: false, resolve: { savedObjects: function ($route, Promise) { @@ -202,7 +204,7 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab }; const history = getHistory(); - // used for restoring background session + // used for restoring a search session let isInitialSearch = true; // search session requested a data refresh @@ -340,6 +342,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; $scope.showSaveQuery = uiCapabilities.discover.saveQuery; + $scope.showTimeCol = + !config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName; let abortController; $scope.$on('$destroy', () => { @@ -414,7 +418,7 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery(); const sort = getSortArray(savedSearch.sort, $scope.indexPattern); - return { + const defaultState = { query, sort: !sort.length ? getDefaultSort($scope.indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc')) @@ -427,6 +431,11 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab interval: 'auto', filters: _.cloneDeep($scope.searchSource.getOwnField('filter')), }; + if (savedSearch.grid) { + defaultState.grid = savedSearch.grid; + } + + return defaultState; } $scope.state.index = $scope.indexPattern.id; @@ -440,6 +449,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, setHeaderActionMenu: getHeaderActionMenuMounter(), + filterManager, + setAppState, data, }; @@ -783,6 +794,17 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex); setAppState({ columns }); }; + + $scope.setColumns = function setColumns(columns) { + // remove first element of columns if it's the configured timeFieldName, which is prepended automatically + const actualColumns = + $scope.indexPattern.timeFieldName && $scope.indexPattern.timeFieldName === columns[0] + ? columns.slice(1) + : columns; + $scope.state = { ...$scope.state, columns: actualColumns }; + setAppState({ columns: actualColumns }); + }; + async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/angular/discover_datagrid.html b/src/plugins/discover/public/application/angular/discover_datagrid.html new file mode 100644 index 0000000000000..e59ebbb0fafd0 --- /dev/null +++ b/src/plugins/discover/public/application/angular/discover_datagrid.html @@ -0,0 +1,31 @@ + + + + diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 7cdcd6cbbca3a..3596c0a2519ed 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -1,6 +1,5 @@
Hello World
; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx index f76e0178e98b0..cf6dc70e92d03 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -25,6 +25,7 @@ import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { ContextErrorMessage } from '../context_error_message'; +import { TopNavMenuMock } from './__mocks__/top_nav_menu'; describe('ContextAppLegacy test', () => { const hit = { @@ -64,6 +65,17 @@ describe('ContextAppLegacy test', () => { onChangeSuccessorCount: jest.fn(), predecessorStatus: 'loaded', successorStatus: 'loaded', + topNavMenu: TopNavMenuMock, + }; + const topNavProps = { + appName: 'context', + showSearchBar: true, + showQueryBar: false, + showFilterBar: true, + showSaveQuery: false, + showDatePicker: false, + indexPatterns: [indexPattern], + useDefaultBehaviors: true, }; it('renders correctly', () => { @@ -72,6 +84,9 @@ describe('ContextAppLegacy test', () => { const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(0); expect(component.find(ActionBar).length).toBe(2); + const topNavMenu = component.find(TopNavMenuMock); + expect(topNavMenu.length).toBe(1); + expect(topNavMenu.props()).toStrictEqual(topNavProps); }); it('renders loading indicator', () => { @@ -82,6 +97,7 @@ describe('ContextAppLegacy test', () => { const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(1); expect(component.find(ActionBar).length).toBe(2); + expect(component.find(TopNavMenuMock).length).toBe(1); }); it('renders error message', () => { @@ -90,6 +106,7 @@ describe('ContextAppLegacy test', () => { props.reason = 'something went wrong'; const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); + expect(component.find(TopNavMenuMock).length).toBe(0); const errorMessage = component.find(ContextErrorMessage); expect(errorMessage.length).toBe(1); }); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index af99c995c60eb..f519df8a0b80d 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -27,8 +27,10 @@ import { import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; import { LOADING_STATUS } from './constants'; import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; +import { TopNavMenuProps } from '../../../../../navigation/public'; export interface ContextAppProps { + topNavMenu: React.ComponentType; columns: string[]; hits: Array>; indexPattern: IIndexPattern; @@ -96,6 +98,20 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { } as DocTableLegacyProps; }; + const TopNavMenu = renderProps.topNavMenu; + const getNavBarProps = () => { + return { + appName: 'context', + showSearchBar: true, + showQueryBar: false, + showFilterBar: true, + showSaveQuery: false, + showDatePicker: false, + indexPatterns: [renderProps.indexPattern], + useDefaultBehaviors: true, + }; + }; + const loadingFeedback = () => { if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) { return ( @@ -112,20 +128,23 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { {isFailed ? ( ) : ( - - - - {loadingFeedback()} - - {isLoaded ? ( -
- -
- ) : null} - - -
-
+
+ + + + + {loadingFeedback()} + + {isLoaded ? ( +
+ +
+ ) : null} + + +
+
+
)} ); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index bc4b7c4babd21..dfb5d90c2befe 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -37,5 +37,6 @@ export function createContextAppLegacy(reactDirective: any) { ['successorAvailable', { watchDepth: 'reference' }], ['successorStatus', { watchDepth: 'reference' }], ['onChangeSuccessorCount', { watchDepth: 'reference' }], + ['topNavMenu', { watchDepth: 'reference' }], ]); } diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts new file mode 100644 index 0000000000000..a146f60652d8a --- /dev/null +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Discover } from './discover'; + +export function createDiscoverDirective(reactDirective: any) { + return reactDirective(Discover, [ + ['fetch', { watchDepth: 'reference' }], + ['fetchCounter', { watchDepth: 'reference' }], + ['fetchError', { watchDepth: 'reference' }], + ['fieldCounts', { watchDepth: 'reference' }], + ['histogramData', { watchDepth: 'reference' }], + ['hits', { watchDepth: 'reference' }], + ['indexPattern', { watchDepth: 'reference' }], + ['onAddColumn', { watchDepth: 'reference' }], + ['onAddFilter', { watchDepth: 'reference' }], + ['onChangeInterval', { watchDepth: 'reference' }], + ['onRemoveColumn', { watchDepth: 'reference' }], + ['onSetColumns', { watchDepth: 'reference' }], + ['onSort', { watchDepth: 'reference' }], + ['opts', { watchDepth: 'reference' }], + ['resetQuery', { watchDepth: 'reference' }], + ['resultState', { watchDepth: 'reference' }], + ['rows', { watchDepth: 'reference' }], + ['searchSource', { watchDepth: 'reference' }], + ['setColumns', { watchDepth: 'reference' }], + ['setIndexPattern', { watchDepth: 'reference' }], + ['showSaveQuery', { watchDepth: 'reference' }], + ['state', { watchDepth: 'reference' }], + ['timefilterUpdateHandler', { watchDepth: 'reference' }], + ['timeRange', { watchDepth: 'reference' }], + ['topNavMenu', { watchDepth: 'reference' }], + ['updateQuery', { watchDepth: 'reference' }], + ['updateSavedQueryId', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx new file mode 100644 index 0000000000000..4e2a2506e282d --- /dev/null +++ b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx @@ -0,0 +1,56 @@ +/* + * 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 * as React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; +import { getServices } from '../../kibana_services'; + +export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( + +)); + +export function DiscoverGridEmbeddable(props: DiscoverGridProps) { + return ( + + + + ); +} + +/** + * this is just needed for the embeddable + */ +export function createDiscoverGridDirective(reactDirective: any) { + return reactDirective(DiscoverGridEmbeddable, [ + ['columns', { watchDepth: 'collection' }], + ['indexPattern', { watchDepth: 'reference' }], + ['onAddColumn', { watchDepth: 'reference', wrapApply: false }], + ['onFilter', { watchDepth: 'reference', wrapApply: false }], + ['onRemoveColumn', { watchDepth: 'reference', wrapApply: false }], + ['onSetColumns', { watchDepth: 'reference', wrapApply: false }], + ['onSort', { watchDepth: 'reference', wrapApply: false }], + ['rows', { watchDepth: 'collection' }], + ['sampleSize', { watchDepth: 'reference' }], + ['searchDescription', { watchDepth: 'reference' }], + ['searchTitle', { watchDepth: 'reference' }], + ['settings', { watchDepth: 'reference' }], + ['showTimeCol', { watchDepth: 'value' }], + ['sort', { watchDepth: 'value' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts index cb3cb06aa90a3..6e5d47be987d8 100644 --- a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts @@ -21,7 +21,6 @@ import { DiscoverLegacy } from './discover_legacy'; export function createDiscoverLegacyDirective(reactDirective: any) { return reactDirective(DiscoverLegacy, [ - ['addColumn', { watchDepth: 'reference' }], ['fetch', { watchDepth: 'reference' }], ['fetchCounter', { watchDepth: 'reference' }], ['fetchError', { watchDepth: 'reference' }], @@ -30,6 +29,7 @@ export function createDiscoverLegacyDirective(reactDirective: any) { ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], ['minimumVisibleRows', { watchDepth: 'reference' }], + ['onAddColumn', { watchDepth: 'reference' }], ['onAddFilter', { watchDepth: 'reference' }], ['onChangeInterval', { watchDepth: 'reference' }], ['onMoveColumn', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.scss b/src/plugins/discover/public/application/components/discover.scss index b17da97a45930..665bd98c232a5 100644 --- a/src/plugins/discover/public/application/components/discover.scss +++ b/src/plugins/discover/public/application/components/discover.scss @@ -35,6 +35,10 @@ discover-app { } } +.dscPageContent { + border: $euiBorderThin; +} + .dscPageContent, .dscPageContent__inner { height: 100%; @@ -46,6 +50,7 @@ discover-app { .dscResultCount { padding: $euiSizeS; + min-height: $euiSize * 3; @include euiBreakpoint('xs', 's') { .dscResultCount__toggle { @@ -76,6 +81,13 @@ discover-app { padding: $euiSizeS; } +// new slimmer layout for data grid +.dscHistogramGrid { + display: flex; + height: $euiSize * 8; + padding: $euiSizeS $euiSizeS 0 $euiSizeS; +} + .dscTable { // SASSTODO: add a monospace modifier to the doc-table component .kbnDocTable__row { diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx new file mode 100644 index 0000000000000..aa756d960e435 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -0,0 +1,321 @@ +/* + * 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 './discover.scss'; +import React, { useState, useRef } from 'react'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHideFor, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import classNames from 'classnames'; +import { HitsCounter } from './hits_counter'; +import { TimechartHeader } from './timechart_header'; +import { getServices } from '../../kibana_services'; +import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; +import { DiscoverNoResults } from './no_results'; +import { LoadingSpinner } from './loading_spinner/loading_spinner'; +import { search } from '../../../../data/public'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './sidebar/discover_sidebar_responsive'; +import { DiscoverProps } from './discover_legacy'; +import { SortPairArr } from '../angular/doc_table/lib/get_sort'; +import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; + +export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( + +)); + +export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( + +)); + +export function Discover({ + fetch, + fetchCounter, + fetchError, + fieldCounts, + histogramData, + hits, + indexPattern, + onAddColumn, + onAddFilter, + onChangeInterval, + onRemoveColumn, + onSetColumns, + onSort, + opts, + resetQuery, + resultState, + rows, + searchSource, + setIndexPattern, + showSaveQuery, + state, + timefilterUpdateHandler, + timeRange, + topNavMenu, + updateQuery, + updateSavedQueryId, +}: DiscoverProps) { + const scrollableDesktop = useRef(null); + const collapseIcon = useRef(null); + const [toggleOn, toggleChart] = useState(true); + const [isSidebarClosed, setIsSidebarClosed] = useState(false); + const services = getServices(); + const { TopNavMenu } = services.navigation.ui; + const { trackUiMetric } = services; + const { savedSearch, indexPatternList, config } = opts; + const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; + const bucketInterval = + bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + ? bucketAggConfig.buckets?.getInterval() + : undefined; + const contentCentered = resultState === 'uninitialized'; + const showTimeCol = !config.get('doc_table:hideTimeColumn', false) && indexPattern.timeFieldName; + const columns = + state.columns && + state.columns.length > 0 && + // check if all columns where removed except the configured timeField (this can't be removed) + !(state.columns.length === 1 && state.columns[0] === indexPattern.timeFieldName) + ? state.columns + : ['_source']; + // if columns include _source this is considered as default view, so you can't remove columns + // until you add a column using Discover's sidebar + const defaultColumns = columns.includes('_source'); + + return ( + + + + +

+ {savedSearch.title} +

+ + + + + + + setIsSidebarClosed(!isSidebarClosed)} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label={i18n.translate('discover.toggleSidebarAriaLabel', { + defaultMessage: 'Toggle sidebar', + })} + buttonRef={collapseIcon} + /> + + + + + {resultState === 'none' && ( + + )} + {resultState === 'uninitialized' && } + {resultState === 'loading' && } + {resultState === 'ready' && ( + + + + + 0 ? hits : 0} + showResetButton={!!(savedSearch && savedSearch.id)} + onResetQuery={resetQuery} + /> + + {toggleOn && ( + + + + )} + + { + toggleChart(!toggleOn); + }} + > + {toggleOn + ? i18n.translate('discover.hideChart', { + defaultMessage: 'Hide chart', + }) + : i18n.translate('discover.showChart', { + defaultMessage: 'Show chart', + })} + + + + + {toggleOn && opts.timefield && ( + +
+ {opts.chartAggConfigs && histogramData && rows.length !== 0 && ( +
+ +
+ )} +
+ +
+ )} + + + + +
+

+ +

+ {rows && rows.length && ( +
+ { + const grid = { ...state.grid } || {}; + const newColumns = { ...grid.columns } || {}; + newColumns[colSettings.columnId] = { + width: colSettings.width, + }; + const newGrid = { ...grid, columns: newColumns }; + opts.setAppState({ grid: newGrid }); + }} + /> +
+ )} +
+
+
+ )} +
+
+
+
+
+
+ ); +} diff --git a/src/plugins/discover/public/application/components/discover_grid/constants.ts b/src/plugins/discover/public/application/components/discover_grid/constants.ts new file mode 100644 index 0000000000000..dec483da8f8a1 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/constants.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ +// data types +export const kibanaJSON = 'kibana-json'; +export const geoPoint = 'geo-point'; +export const unknownType = 'unknown'; +export const gridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + rowHover: 'none', +}; + +export const pageSizeArr = [25, 50, 100]; +export const defaultPageSize = 25; +export const toolbarVisibility = { + showColumnSelector: { + allowHide: false, + allowReorder: true, + }, + showStyleSelector: false, +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss new file mode 100644 index 0000000000000..64a7eda963349 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -0,0 +1,68 @@ +.dscDiscoverGrid { + width: 100%; + max-width: 100%; + height: 100%; + overflow: hidden; + + .euiDataGrid__controls { + border: none; + border-bottom: $euiBorderThin; + } + + .euiDataGridRowCell:first-of-type, + .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type { + border-left: none; + border-right: none; + } + + .euiDataGridRowCell:last-of-type, + .euiDataGridHeaderCell:last-of-type { + border-right: none; + } +} + +.dscDiscoverGrid__footer { + background-color: $euiColorLightShade; + padding: $euiSize / 2 $euiSize; + margin-top: $euiSize / 4; + text-align: center; +} + +.dscTable__flyoutHeader { + white-space: nowrap; +} + +// We only truncate if the cell is not a control column. +.euiDataGridHeader { + .euiDataGridHeaderCell__content { + @include euiTextTruncate; + overflow: hidden; + white-space: nowrap; + flex-grow: 1; + } + + .euiDataGridHeaderCell__popover { + flex-grow: 0; + flex-basis: auto; + width: auto; + padding-left: $euiSizeXS; + } +} + +.euiDataGridRowCell--numeric { + text-align: right; +} + +.euiDataGrid__noResults { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1 0 100%; + text-align: center; + height: 100%; + width: 100%; +} + +.dscFormatSource { + @include euiTextTruncate; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx new file mode 100644 index 0000000000000..9588f74ed2bc2 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -0,0 +1,336 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import './discover_grid.scss'; +import { + EuiDataGridSorting, + EuiDataGridStyle, + EuiDataGridProps, + EuiDataGrid, + EuiIcon, + EuiScreenReaderOnly, + EuiSpacer, + EuiText, + htmlIdGenerator, +} from '@elastic/eui'; +import { IndexPattern } from '../../../kibana_services'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { getPopoverContents, getSchemaDetectors } from './discover_grid_schema'; +import { DiscoverGridFlyout } from './discover_grid_flyout'; +import { DiscoverGridContext } from './discover_grid_context'; +import { getRenderCellValueFn } from './get_render_cell_value'; +import { DiscoverGridSettings } from './types'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { + getEuiGridColumns, + getLeadControlColumns, + getVisibleColumns, +} from './discover_grid_columns'; +import { defaultPageSize, gridStyle, pageSizeArr, toolbarVisibility } from './constants'; +import { DiscoverServices } from '../../../build_services'; + +interface SortObj { + id: string; + direction: string; +} + +export interface DiscoverGridProps { + /** + * Determines which element labels the grid for ARIA + */ + ariaLabelledBy: string; + /** + * Determines which columns are displayed + */ + columns: string[]; + /** + * Determines whether the given columns are the default ones, so parts of the document + * are displayed (_source) with limited actions (cannor move, remove columns) + * Implemented for matching with legacy behavior + */ + defaultColumns: boolean; + /** + * The used index pattern + */ + indexPattern: IndexPattern; + /** + * Function used to add a column in the document flyout + */ + onAddColumn: (column: string) => void; + /** + * Function to add a filter in the grid cell or document flyout + */ + onFilter: DocViewFilterFn; + /** + * Function used in the grid header and flyout to remove a column + * @param column + */ + onRemoveColumn: (column: string) => void; + /** + * Function triggered when a column is resized by the user + */ + onResize?: (colSettings: { columnId: string; width: number }) => void; + /** + * Function to set all columns + */ + onSetColumns: (columns: string[]) => void; + /** + * function to change sorting of the documents + */ + onSort: (sort: string[][]) => void; + /** + * Array of documents provided by Elasticsearch + */ + rows?: ElasticSearchHit[]; + /** + * The max size of the documents returned by Elasticsearch + */ + sampleSize: number; + /** + * Grid display settings persisted in Elasticsearch (e.g. column width) + */ + settings?: DiscoverGridSettings; + /** + * Saved search description + */ + searchDescription?: string; + /** + * Saved search title + */ + searchTitle?: string; + /** + * Discover plugin services + */ + services: DiscoverServices; + /** + * Determines whether the time columns should be displayed (legacy settings) + */ + showTimeCol: boolean; + /** + * Current sort setting + */ + sort: SortPairArr[]; +} + +export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { + return ; +}); + +export const DiscoverGrid = ({ + ariaLabelledBy, + columns, + defaultColumns, + indexPattern, + onAddColumn, + onFilter, + onRemoveColumn, + onResize, + onSetColumns, + onSort, + rows, + sampleSize, + searchDescription, + searchTitle, + services, + settings, + showTimeCol, + sort, +}: DiscoverGridProps) => { + const [expanded, setExpanded] = useState(undefined); + + /** + * Pagination + */ + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: defaultPageSize }); + const rowCount = useMemo(() => (rows ? rows.length : 0), [rows]); + const pageCount = useMemo(() => Math.ceil(rowCount / pagination.pageSize), [ + rowCount, + pagination, + ]); + const isOnLastPage = pagination.pageIndex === pageCount - 1; + + const paginationObj = useMemo(() => { + const onChangeItemsPerPage = (pageSize: number) => + setPagination((paginationData) => ({ ...paginationData, pageSize })); + + const onChangePage = (pageIndex: number) => + setPagination((paginationData) => ({ ...paginationData, pageIndex })); + + return { + onChangeItemsPerPage, + onChangePage, + pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: pageSizeArr, + }; + }, [pagination, pageCount]); + + /** + * Sorting + */ + const sortingColumns = useMemo(() => sort.map(([id, direction]) => ({ id, direction })), [sort]); + + const onTableSort = useCallback( + (sortingColumnsData) => { + onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + }, + [onSort] + ); + + /** + * Cell rendering + */ + const renderCellValue = useMemo( + () => + getRenderCellValueFn( + indexPattern, + rows, + rows ? rows.map((hit) => indexPattern.flattenHit(hit)) : [] + ), + [rows, indexPattern] + ); + + /** + * Render variables + */ + const showDisclaimer = rowCount === sampleSize && isOnLastPage; + const randomId = useMemo(() => htmlIdGenerator()(), []); + + const euiGridColumns = useMemo( + () => getEuiGridColumns(columns, settings, indexPattern, showTimeCol, defaultColumns), + [columns, indexPattern, showTimeCol, settings, defaultColumns] + ); + const schemaDetectors = useMemo(() => getSchemaDetectors(), []); + const popoverContents = useMemo(() => getPopoverContents(), []); + const columnsVisibility = useMemo( + () => ({ + visibleColumns: getVisibleColumns(columns, indexPattern, showTimeCol) as string[], + setVisibleColumns: (newColumns: string[]) => { + onSetColumns(newColumns); + }, + }), + [columns, indexPattern, showTimeCol, onSetColumns] + ); + const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [ + sortingColumns, + onTableSort, + ]); + const lead = useMemo(() => getLeadControlColumns(), []); + + if (!rowCount) { + return ( +
+ + + + + +
+ ); + } + + return ( + + <> + { + if (onResize) { + onResize(col); + } + }} + pagination={paginationObj} + popoverContents={popoverContents} + renderCellValue={renderCellValue} + rowCount={rowCount} + schemaDetectors={schemaDetectors} + sorting={sorting as EuiDataGridSorting} + toolbarVisibility={ + defaultColumns + ? { + ...toolbarVisibility, + showColumnSelector: false, + } + : toolbarVisibility + } + /> + + {showDisclaimer && ( +

+ + + + +

+ )} + {searchTitle && ( + +

+ {searchDescription ? ( + + ) : ( + + )} +

+
+ )} + {expanded && ( + setExpanded(undefined)} + services={services} + /> + )} + +
+ ); +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx new file mode 100644 index 0000000000000..a85583f66c6fa --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -0,0 +1,80 @@ +/* + * 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 { mountWithIntl } from '@kbn/test/jest'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { FilterInBtn, FilterOutBtn } from './discover_grid_cell_actions'; +import { DiscoverGridContext } from './discover_grid_context'; + +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { esHits } from '../../../__mocks__/es_hits'; +import { EuiButton } from '@elastic/eui'; + +describe('Discover cell actions ', function () { + it('triggers filter function when FilterInBtn is clicked', async () => { + const contextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + } + rowIndex={1} + columnId={'extension'} + isExpanded={false} + closePopover={jest.fn()} + /> + + ); + const button = findTestSubject(component, 'filterForButton'); + await button.simulate('click'); + expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '+'); + }); + it('triggers filter function when FilterOutBtn is clicked', async () => { + const contextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + } + rowIndex={1} + columnId={'extension'} + isExpanded={false} + closePopover={jest.fn()} + /> + + ); + const button = findTestSubject(component, 'filterOutButton'); + await button.simulate('click'); + expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '-'); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx new file mode 100644 index 0000000000000..ef56166258c9b --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx @@ -0,0 +1,97 @@ +/* + * 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, { useContext } from 'react'; +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IndexPatternField } from '../../../../../data/common/index_patterns/fields'; +import { DiscoverGridContext } from './discover_grid_context'; + +export const FilterInBtn = ({ + Component, + rowIndex, + columnId, +}: EuiDataGridColumnCellActionProps) => { + const context = useContext(DiscoverGridContext); + const buttonTitle = i18n.translate('discover.grid.filterForAria', { + defaultMessage: 'Filter for this {value}', + values: { value: columnId }, + }); + + return ( + { + const row = context.rows[rowIndex]; + const flattened = context.indexPattern.flattenHit(row); + + if (flattened) { + context.onFilter(columnId, flattened[columnId], '+'); + } + }} + iconType="plusInCircle" + aria-label={buttonTitle} + title={buttonTitle} + data-test-subj="filterForButton" + > + {i18n.translate('discover.grid.filterFor', { + defaultMessage: 'Filter for', + })} + + ); +}; + +export const FilterOutBtn = ({ + Component, + rowIndex, + columnId, +}: EuiDataGridColumnCellActionProps) => { + const context = useContext(DiscoverGridContext); + const buttonTitle = i18n.translate('discover.grid.filterOutAria', { + defaultMessage: 'Filter out this {value}', + values: { value: columnId }, + }); + + return ( + { + const row = context.rows[rowIndex]; + const flattened = context.indexPattern.flattenHit(row); + + if (flattened) { + context.onFilter(columnId, flattened[columnId], '-'); + } + }} + iconType="minusInCircle" + aria-label={buttonTitle} + title={buttonTitle} + data-test-subj="filterOutButton" + > + {i18n.translate('discover.grid.filterOut', { + defaultMessage: 'Filter out', + })} + + ); +}; + +export function buildCellActions(field: IndexPatternField) { + if (!field.aggregatable && !field.searchable) { + return undefined; + } + + return [FilterInBtn, FilterOutBtn]; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx new file mode 100644 index 0000000000000..dad7e1363fdd9 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { getEuiGridColumns } from './discover_grid_columns'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; + +describe('Discover grid columns ', function () { + it('returns eui grid columns without time column', async () => { + const actual = getEuiGridColumns(['extension', 'message'], {}, indexPatternMock, false, false); + expect(actual).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "extension", + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "message", + "isSortable": undefined, + "schema": "unknown", + }, + ] + `); + }); + it('returns eui grid columns without time column showing default columns', async () => { + const actual = getEuiGridColumns( + ['extension', 'message'], + {}, + indexPatternWithTimefieldMock, + false, + true + ); + expect(actual).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "extension", + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "message", + "isSortable": undefined, + "schema": "unknown", + }, + ] + `); + }); + it('returns eui grid columns with time column', async () => { + const actual = getEuiGridColumns( + ['extension', 'message'], + {}, + indexPatternWithTimefieldMock, + true, + false + ); + expect(actual).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": false, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": "Time (timestamp)", + "id": "timestamp", + "initialWidth": 180, + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "extension", + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "message", + "isSortable": undefined, + "schema": "unknown", + }, + ] + `); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx new file mode 100644 index 0000000000000..1cf9c84405a61 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiDataGridColumn, EuiScreenReaderOnly } from '@elastic/eui'; +import { ExpandButton } from './discover_grid_expand_button'; +import { DiscoverGridSettings } from './types'; +import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; +import { buildCellActions } from './discover_grid_cell_actions'; +import { getSchemaByKbnType } from './discover_grid_schema'; + +export function getLeadControlColumns() { + return [ + { + id: 'openDetails', + width: 32, + headerCellRender: () => ( + + + {i18n.translate('discover.controlColumnHeader', { + defaultMessage: 'Control column', + })} + + + ), + rowCellRender: ExpandButton, + }, + ]; +} + +export function buildEuiGridColumn( + columnName: string, + columnWidth: number | undefined = 0, + indexPattern: IndexPattern, + defaultColumns: boolean +) { + const timeString = i18n.translate('discover.timeLabel', { + defaultMessage: 'Time', + }); + const indexPatternField = indexPattern.getFieldByName(columnName); + const column: EuiDataGridColumn = { + id: columnName, + schema: getSchemaByKbnType(indexPatternField?.type), + isSortable: indexPatternField?.sortable, + display: indexPatternField?.displayName, + actions: { + showHide: + defaultColumns || columnName === indexPattern.timeFieldName + ? false + : { + label: i18n.translate('discover.removeColumnLabel', { + defaultMessage: 'Remove column', + }), + iconType: 'cross', + }, + showMoveLeft: !defaultColumns, + showMoveRight: !defaultColumns, + }, + cellActions: indexPatternField ? buildCellActions(indexPatternField) : [], + }; + + if (column.id === indexPattern.timeFieldName) { + column.display = `${timeString} (${indexPattern.timeFieldName})`; + column.initialWidth = 180; + } + if (columnWidth > 0) { + column.initialWidth = Number(columnWidth); + } + return column; +} + +export function getEuiGridColumns( + columns: string[], + settings: DiscoverGridSettings | undefined, + indexPattern: IndexPattern, + showTimeCol: boolean, + defaultColumns: boolean +) { + const timeFieldName = indexPattern.timeFieldName; + const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; + + if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) { + const usedColumns = [indexPattern.timeFieldName, ...columns]; + return usedColumns.map((column) => + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + ); + } + + return columns.map((column) => + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + ); +} + +export function getVisibleColumns( + columns: string[], + indexPattern: IndexPattern, + showTimeCol: boolean +) { + const timeFieldName = indexPattern.timeFieldName; + + if (showTimeCol && !columns.find((col) => col === timeFieldName)) { + return [timeFieldName, ...columns]; + } + + return columns; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx new file mode 100644 index 0000000000000..dcc404a0e48df --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx @@ -0,0 +1,34 @@ +/* + * 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 { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { IndexPattern } from '../../../kibana_services'; + +export interface GridContext { + expanded: ElasticSearchHit | undefined; + setExpanded: (hit: ElasticSearchHit | undefined) => void; + rows: ElasticSearchHit[]; + onFilter: DocViewFilterFn; + indexPattern: IndexPattern; + isDarkMode: boolean; +} + +const defaultContext = ({} as unknown) as GridContext; + +export const DiscoverGridContext = React.createContext(defaultContext); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx new file mode 100644 index 0000000000000..82fcad8c2cd6f --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 { mountWithIntl } from '@kbn/test/jest'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { ExpandButton } from './discover_grid_expand_button'; +import { DiscoverGridContext } from './discover_grid_context'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { esHits } from '../../../__mocks__/es_hits'; + +describe('Discover grid view button ', function () { + it('when no document is expanded, setExpanded is called with current document', async () => { + const contextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + + + ); + const button = findTestSubject(component, 'docTableExpandToggleColumn'); + await button.simulate('click'); + expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[0]); + }); + it('when the current document is expanded, setExpanded is called with undefined', async () => { + const contextMock = { + expanded: esHits[0], + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + + + ); + const button = findTestSubject(component, 'docTableExpandToggleColumn'); + await button.simulate('click'); + expect(contextMock.setExpanded).toHaveBeenCalledWith(undefined); + }); + it('when another document is expanded, setExpanded is called with the current document', async () => { + const contextMock = { + expanded: esHits[0], + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + + + ); + const button = findTestSubject(component, 'docTableExpandToggleColumn'); + await button.simulate('click'); + expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[1]); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx new file mode 100644 index 0000000000000..d4a3fe85e34ef --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx @@ -0,0 +1,62 @@ +/* + * 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, { useContext, useEffect } from 'react'; +import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; +import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import themeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { DiscoverGridContext } from './discover_grid_context'; +/** + * Button to expand a given row + */ +export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { + const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext); + const current = rows[rowIndex]; + useEffect(() => { + if (expanded && current && expanded._id === current._id) { + setCellProps({ + style: { + backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); + } + }, [expanded, current, setCellProps, isDarkMode]); + + const isCurrentRowExpanded = current === expanded; + const buttonLabel = i18n.translate('discover.grid.viewDoc', { + defaultMessage: 'Toggle dialog with details', + }); + + return ( + + setExpanded(isCurrentRowExpanded ? undefined : current)} + color={isCurrentRowExpanded ? 'primary' : 'subdued'} + iconType={isCurrentRowExpanded ? 'minimize' : 'expand'} + isSelected={isCurrentRowExpanded} + /> + + ); +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx new file mode 100644 index 0000000000000..79ad98ae2babe --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -0,0 +1,143 @@ +/* + * 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, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiButtonEmpty, + EuiText, + EuiSpacer, + EuiPortal, +} from '@elastic/eui'; +import { DocViewer } from '../doc_viewer/doc_viewer'; +import { IndexPattern } from '../../../kibana_services'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverServices } from '../../../build_services'; +import { getContextUrl } from '../../helpers/get_context_url'; + +interface Props { + columns: string[]; + hit: ElasticSearchHit; + indexPattern: IndexPattern; + onAddColumn: (column: string) => void; + onClose: () => void; + onFilter: DocViewFilterFn; + onRemoveColumn: (column: string) => void; + services: DiscoverServices; +} + +/** + * Flyout displaying an expanded Elasticsearch document + */ +export function DiscoverGridFlyout({ + hit, + indexPattern, + columns, + onFilter, + onClose, + onRemoveColumn, + onAddColumn, + services, +}: Props) { + return ( + + + + +

+ {i18n.translate('discover.grid.tableRow.detailHeading', { + defaultMessage: 'Expanded document', + })} +

+
+ + + + + + + {i18n.translate('discover.grid.tableRow.viewText', { + defaultMessage: 'View:', + })} + + + + + + {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', { + defaultMessage: 'Single document', + })} + + + {indexPattern.isTimeBased() && indexPattern.id && ( + + + {i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', { + defaultMessage: 'Surrounding documents', + })} + + + )} + +
+ + { + onFilter(mapping, value, mode); + onClose(); + }} + onRemoveColumn={(columnName: string) => { + onRemoveColumn(columnName); + onClose(); + }} + onAddColumn={(columnName: string) => { + onAddColumn(columnName); + onClose(); + }} + /> + +
+
+ ); +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx new file mode 100644 index 0000000000000..aa87d3982fa06 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx @@ -0,0 +1,103 @@ +/* + * 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, { ReactNode } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; +import { geoPoint, kibanaJSON, unknownType } from './constants'; +import { KBN_FIELD_TYPES } from '../../../../../data/common'; + +export function getSchemaByKbnType(kbnType: string | undefined) { + // Default DataGrid schemas: boolean, numeric, datetime, json, currency, string + switch (kbnType) { + case KBN_FIELD_TYPES.IP: + case KBN_FIELD_TYPES.GEO_SHAPE: + case KBN_FIELD_TYPES.NUMBER: + return 'numeric'; + case KBN_FIELD_TYPES.BOOLEAN: + return 'boolean'; + case KBN_FIELD_TYPES.STRING: + return 'string'; + case KBN_FIELD_TYPES.DATE: + return 'datetime'; + case KBN_FIELD_TYPES._SOURCE: + return kibanaJSON; + case KBN_FIELD_TYPES.GEO_POINT: + return geoPoint; + default: + return unknownType; + } +} + +export function getSchemaDetectors() { + return [ + { + type: kibanaJSON, + detector() { + return 0; // this schema is always explicitly defined + }, + sortTextAsc: '', + sortTextDesc: '', + icon: '', + color: '', + }, + { + type: unknownType, + detector() { + return 0; // this schema is always explicitly defined + }, + sortTextAsc: '', + sortTextDesc: '', + icon: '', + color: '', + }, + { + type: geoPoint, + detector() { + return 0; // this schema is always explicitly defined + }, + sortTextAsc: '', + sortTextDesc: '', + icon: 'tokenGeo', + }, + ]; +} + +/** + * Returns custom popover content for certain schemas + */ +export function getPopoverContents() { + return { + [geoPoint]: ({ children }: { children: ReactNode }) => { + return {children}; + }, + [unknownType]: ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); + }, + [kibanaJSON]: ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); + }, + }; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx new file mode 100644 index 0000000000000..d9896f4c53907 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx @@ -0,0 +1,132 @@ +/* + * 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 { shallow } from 'enzyme'; +import { getRenderCellValueFn } from './get_render_cell_value'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +const rows = [ + { + _id: '1', + _index: 'test', + _type: 'test', + _score: 1, + _source: { bytes: 100 }, + }, +]; + +describe('Discover grid cell rendering', function () { + it('renders bytes column correctly', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(`"100"`); + }); + it('renders _source column correctly', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot( + `"
bytes
100
"` + ); + }); + + it('renders _source column correctly when isDetails is set to true', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(` + "{ + "bytes": 100 + }" + `); + }); + + it('renders correctly when invalid row is given', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(`"-"`); + }); + it('renders correctly when invalid column is given', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(`"-"`); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx new file mode 100644 index 0000000000000..2157e778f84db --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -0,0 +1,116 @@ +/* + * 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, { Fragment, useContext, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import themeLight from '@elastic/eui/dist/eui_theme_light.json'; +import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; + +import { + EuiDataGridCellValueElementProps, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { IndexPattern } from '../../../kibana_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverGridContext } from './discover_grid_context'; + +export const getRenderCellValueFn = ( + indexPattern: IndexPattern, + rows: ElasticSearchHit[] | undefined, + rowsFlattened: Array> +) => ({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => { + const row = rows ? (rows[rowIndex] as Record) : undefined; + const rowFlattened = rowsFlattened + ? (rowsFlattened[rowIndex] as Record) + : undefined; + + const field = indexPattern.fields.getByName(columnId); + const ctx = useContext(DiscoverGridContext); + + useEffect(() => { + if (ctx.expanded && row && ctx.expanded._id === row._id) { + setCellProps({ + style: { + backgroundColor: ctx.isDarkMode + ? themeDark.euiColorHighlight + : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); + } + }, [ctx, row, setCellProps]); + + if (typeof row === 'undefined' || typeof rowFlattened === 'undefined') { + return -; + } + + if (field && field.type === '_source') { + if (isDetails) { + // nicely formatted JSON for the expanded view + return {JSON.stringify(row[columnId], null, 2)}; + } + const formatted = indexPattern.formatHit(row); + + return ( + + {Object.keys(formatted).map((key) => ( + + {key} + + + ))} + + ); + } + + if (!field?.type && rowFlattened && typeof rowFlattened[columnId] === 'object') { + if (isDetails) { + // nicely formatted JSON for the expanded view + return {JSON.stringify(rowFlattened[columnId], null, 2)}; + } + + return {JSON.stringify(rowFlattened[columnId])}; + } + + if (field?.type === 'geo_point' && rowFlattened && rowFlattened[columnId]) { + const valueFormatted = rowFlattened[columnId] as { lat: number; lon: number }; + return ( +
+ {i18n.translate('discover.latitudeAndLongitude', { + defaultMessage: 'Lat: {lat} Lon: {lon}', + values: { + lat: valueFormatted?.lat, + lon: valueFormatted?.lon, + }, + })} +
+ ); + } + + const valueFormatted = indexPattern.formatField(row, columnId); + if (typeof valueFormatted === 'undefined') { + return -; + } + return ( + // eslint-disable-next-line react/no-danger + + ); +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/types.ts b/src/plugins/discover/public/application/components/discover_grid/types.ts new file mode 100644 index 0000000000000..3d57dbffe924e --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/types.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. + */ + +/** + * User configurable state of data grid, persisted in saved search + */ +export interface DiscoverGridSettings { + columns?: Record; +} + +export interface DiscoverGridSettingsColumn { + width?: number; +} diff --git a/src/plugins/discover/public/application/components/discover_legacy.test.tsx b/src/plugins/discover/public/application/components/discover_legacy.test.tsx index e2f4ba7ab6e2e..bad5c1d2e532d 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.test.tsx @@ -67,7 +67,6 @@ function getProps(indexPattern: IndexPattern) { } as unknown) as DiscoverServices; return { - addColumn: jest.fn(), fetch: jest.fn(), fetchCounter: 0, fetchError: undefined, @@ -75,6 +74,7 @@ function getProps(indexPattern: IndexPattern) { hits: esHits.length, indexPattern, minimumVisibleRows: 10, + onAddColumn: jest.fn(), onAddFilter: jest.fn(), onChangeInterval: jest.fn(), onMoveColumn: jest.fn(), diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index d228be66990bd..436a145024437 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -63,46 +63,161 @@ import { import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; export interface DiscoverProps { - addColumn: (column: string) => void; + /** + * Function to fetch documents from Elasticsearch + */ fetch: () => void; + /** + * Counter how often data was fetched (used for testing) + */ fetchCounter: number; + /** + * Error in case of a failing document fetch + */ fetchError?: Error; + /** + * Statistics by fields calculated using the fetched documents + */ fieldCounts: Record; + /** + * Histogram aggregation data + */ histogramData?: Chart; + /** + * Number of documents found by recent fetch + */ hits: number; + /** + * Current IndexPattern + */ indexPattern: IndexPattern; + /** + * Value needed for legacy "infinite" loading functionality + * Determins how much records are rendered using the legacy table + * Increased when scrolling down + */ minimumVisibleRows: number; + /** + * Function to add a column to state + */ + onAddColumn: (column: string) => void; + /** + * Function to add a filter to state + */ onAddFilter: DocViewFilterFn; + /** + * Function to change the used time interval of the date histogram + */ onChangeInterval: (interval: string) => void; + /** + * Function to move a given column to a given index, used in legacy table + */ onMoveColumn: (columns: string, newIdx: number) => void; + /** + * Function to remove a given column from state + */ onRemoveColumn: (column: string) => void; + /** + * Function to replace columns in state + */ onSetColumns: (columns: string[]) => void; + /** + * Function to scroll down the legacy table to the bottom + */ onSkipBottomButtonClick: () => void; + /** + * Function to change sorting of the table, triggers a fetch + */ onSort: (sort: string[][]) => void; opts: { + /** + * Date histogram aggregation config + */ chartAggConfigs?: AggConfigs; + /** + * Client of uiSettings + */ config: IUiSettingsClient; + /** + * Data plugin + */ data: DataPublicPluginStart; - fixedScroll: (el: HTMLElement) => void; + /** + * Data plugin filter manager + */ filterManager: FilterManager; + /** + * List of available index patterns + */ indexPatternList: Array>; + /** + * The number of documents that can be displayed in the table/grid + */ sampleSize: number; + /** + * Current instance of SavedSearch + */ savedSearch: SavedSearch; + /** + * Function to set the header menu + */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Timefield of the currently used index pattern + */ timefield: string; + /** + * Function to set the current state + */ setAppState: (state: Partial) => void; }; + /** + * Function to reset the current query + */ resetQuery: () => void; + /** + * Current state of the actual query, one of 'uninitialized', 'loading' ,'ready', 'none' + */ resultState: string; + /** + * Array of document of the recent successful search request + */ rows: ElasticSearchHit[]; + /** + * Instance of SearchSource, the high level search API + */ searchSource: ISearchSource; + /** + * Function to change the current index pattern + */ setIndexPattern: (id: string) => void; + /** + * Determines whether the user should be able to use the save query feature + */ showSaveQuery: boolean; + /** + * Current app state of URL + */ state: AppState; + /** + * Function to update the time filter + */ timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; + /** + * Currently selected time range + */ timeRange?: { from: string; to: string }; + /** + * Menu data of top navigation (New, save ...) + */ topNavMenu: TopNavMenuData[]; + /** + * Function to update the actual query + */ updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + /** + * Function to update the actual savedQuery id + */ updateSavedQueryId: (savedQueryId?: string) => void; } @@ -114,7 +229,6 @@ export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps )); export function DiscoverLegacy({ - addColumn, fetch, fetchCounter, fieldCounts, @@ -123,6 +237,7 @@ export function DiscoverLegacy({ hits, indexPattern, minimumVisibleRows, + onAddColumn, onAddFilter, onChangeInterval, onMoveColumn, @@ -192,7 +307,7 @@ export function DiscoverLegacy({ fieldCounts={fieldCounts} hits={rows} indexPatternList={indexPatternList} - onAddField={addColumn} + onAddField={onAddColumn} onAddFilter={onAddFilter} onRemoveField={onRemoveColumn} selectedIndexPattern={searchSource && searchSource.getField('index')} @@ -206,6 +321,8 @@ export function DiscoverLegacy({ setIsSidebarClosed(!isSidebarClosed)} data-test-subj="collapseSideBarButton" aria-controls="discover-sidebar" @@ -335,7 +452,7 @@ export function DiscoverLegacy({ sort={state.sort || []} searchDescription={opts.savedSearch.description} searchTitle={opts.savedSearch.lastSavedTitle} - onAddColumn={addColumn} + onAddColumn={onAddColumn} onFilter={onAddFilter} onMoveColumn={onMoveColumn} onRemoveColumn={onRemoveColumn} diff --git a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap index b5bd961037e21..d02b484a06a49 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap @@ -6,6 +6,7 @@ exports[`Render with 3 different tabs 1`] = ` > - +
); } diff --git a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap index 2fa96f9372380..6b5e45f8a0448 100644 --- a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -31,7 +31,7 @@ exports[`FieldName renders a geo field 1`] = `
`; -exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = ` +exports[`FieldName renders a number field by providing a field record 1`] = `
diff --git a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx index 0deddce1c40a8..248191acf9ab9 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx @@ -27,7 +27,7 @@ test('FieldName renders a string field by providing fieldType and fieldName', () expect(component).toMatchSnapshot(); }); -test('FieldName renders a number field by providing a field record, useShortDots is set to false', () => { +test('FieldName renders a number field by providing a field record', () => { const component = render(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js b/src/plugins/discover/public/application/components/help_menu/help_menu_util.js index 03ab44966f796..e3c2adf587d91 100644 --- a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js +++ b/src/plugins/discover/public/application/components/help_menu/help_menu_util.js @@ -29,7 +29,7 @@ export function addHelpMenuToAppChrome(chrome) { links: [ { linkType: 'documentation', - href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/discover.html`, + href: `${docLinks.links.discover.guide}`, }, ], }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 391e15485f074..0957ee101bd27 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -56,7 +56,6 @@ function getComponent({ }: { selected?: boolean; showDetails?: boolean; - useShortDots?: boolean; field?: IndexPatternField; }) { const indexPattern = getStubIndexPattern( diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts index d4670a1e76011..22cacae4c3b45 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts @@ -19,51 +19,58 @@ import { groupFields } from './group_fields'; import { getDefaultFieldFilter } from './field_filter'; +import { IndexPatternField } from '../../../../../../data/common/index_patterns/fields'; -describe('group_fields', function () { - it('should group fields in selected, popular, unpopular group', function () { - const fields = [ - { - name: 'category', - type: 'string', - esTypes: ['text'], - count: 1, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - name: 'currency', - type: 'string', - esTypes: ['keyword'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - name: 'customer_birth_date', - type: 'date', - esTypes: ['date'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - ]; +const fields = [ + { + name: 'category', + type: 'string', + esTypes: ['text'], + count: 1, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'currency', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'customer_birth_date', + type: 'date', + esTypes: ['date'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, +]; - const fieldCounts = { - category: 1, - currency: 1, - customer_birth_date: 1, - }; +const fieldCounts = { + category: 1, + currency: 1, + customer_birth_date: 1, +}; +describe('group_fields', function () { + it('should group fields in selected, popular, unpopular group', function () { const fieldFilterState = getDefaultFieldFilter(); - const actual = groupFields(fields as any, ['currency'], 5, fieldCounts, fieldFilterState); + const actual = groupFields( + fields as IndexPatternField[], + ['currency'], + 5, + fieldCounts, + fieldFilterState + ); expect(actual).toMatchInlineSnapshot(` Object { "popular": Array [ @@ -111,4 +118,34 @@ describe('group_fields', function () { } `); }); + + it('should sort selected fields by columns order ', function () { + const fieldFilterState = getDefaultFieldFilter(); + + const actual1 = groupFields( + fields as IndexPatternField[], + ['customer_birth_date', 'currency', 'unknown'], + 5, + fieldCounts, + fieldFilterState + ); + expect(actual1.selected.map((field) => field.name)).toEqual([ + 'customer_birth_date', + 'currency', + 'unknown', + ]); + + const actual2 = groupFields( + fields as IndexPatternField[], + ['currency', 'customer_birth_date', 'unknown'], + 5, + fieldCounts, + fieldFilterState + ); + expect(actual2.selected.map((field) => field.name)).toEqual([ + 'currency', + 'customer_birth_date', + 'unknown', + ]); + }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx index c6a06618900fd..c34becc97cb93 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx @@ -70,6 +70,15 @@ export function groupFields( result.unpopular.push(field); } } + // add columns, that are not part of the index pattern, to be removeable + for (const column of columns) { + if (!result.selected.find((field) => field.name === column)) { + result.selected.push({ name: column, displayName: column } as IndexPatternField); + } + } + result.selected.sort((a, b) => { + return columns.indexOf(a.name) - columns.indexOf(b.name); + }); return result; } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index d0c3907d31242..e4a8ab7bc67ff 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -36,6 +36,7 @@ import { import { Container, Embeddable } from '../../../../embeddable/public'; import * as columnActions from '../angular/doc_table/actions/columns'; import searchTemplate from './search_template.html'; +import searchTemplateGrid from './search_template_datagrid.html'; import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { getSortForSearchSource } from '../angular/doc_table'; @@ -49,23 +50,29 @@ import { import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearch } from '../..'; import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; +import { DiscoverGridSettings } from '../components/discover_grid/types'; +import { DiscoverServices } from '../../build_services'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort'; interface SearchScope extends ng.IScope { columns?: string[]; + settings?: DiscoverGridSettings; description?: string; sort?: SortOrder[]; sharedItemTitle?: string; inspectorAdapters?: Adapters; setSortOrder?: (sortPair: SortOrder[]) => void; + setColumns?: (columns: string[]) => void; removeColumn?: (column: string) => void; addColumn?: (column: string) => void; moveColumn?: (column: string, index: number) => void; filter?: (field: IFieldType, value: string[], operator: string) => void; - hits?: any[]; + hits?: ElasticSearchHit[]; indexPattern?: IndexPattern; totalHitCount?: number; isLoading?: boolean; + showTimeCol?: boolean; } interface SearchEmbeddableConfig { @@ -77,6 +84,7 @@ interface SearchEmbeddableConfig { indexPatterns?: IndexPattern[]; editable: boolean; filterManager: FilterManager; + services: DiscoverServices; } export class SearchEmbeddable @@ -95,6 +103,7 @@ export class SearchEmbeddable public readonly type = SEARCH_EMBEDDABLE_TYPE; private filterManager: FilterManager; private abortController?: AbortController; + private services: DiscoverServices; private prevTimeRange?: TimeRange; private prevFilters?: Filter[]; @@ -111,6 +120,7 @@ export class SearchEmbeddable indexPatterns, editable, filterManager, + services, }: SearchEmbeddableConfig, initialInput: SearchInput, private readonly executeTriggerActions: UiActionsStart['executeTriggerActions'], @@ -128,7 +138,7 @@ export class SearchEmbeddable }, parent ); - + this.services = services; this.filterManager = filterManager; this.savedSearch = savedSearch; this.$rootScope = $rootScope; @@ -138,8 +148,8 @@ export class SearchEmbeddable }; this.initializeSearchScope(); - this.autoRefreshFetchSubscription = getServices() - .timefilter.getAutoRefreshFetch$() + this.autoRefreshFetchSubscription = this.services.timefilter + .getAutoRefreshFetch$() .subscribe(this.fetch); this.subscription = this.getUpdated$().subscribe(() => { @@ -167,7 +177,9 @@ export class SearchEmbeddable if (!this.searchScope) { throw new Error('Search scope not defined'); } - this.searchInstance = this.$compile(searchTemplate)(this.searchScope); + this.searchInstance = this.$compile( + this.services.uiSettings.get('doc_table:legacy', true) ? searchTemplate : searchTemplateGrid + )(this.searchScope); const rootNode = angular.element(domNode); rootNode.append(this.searchInstance); @@ -250,6 +262,15 @@ export class SearchEmbeddable this.updateInput({ columns }); }; + searchScope.setColumns = (columns: string[]) => { + this.updateInput({ columns }); + }; + + if (this.savedSearch.grid) { + searchScope.settings = this.savedSearch.grid; + } + searchScope.showTimeCol = !this.services.uiSettings.get('doc_table:hideTimeColumn', false); + searchScope.filter = async (field, value, operator) => { let filters = esFilters.generateFilters( this.filterManager, @@ -286,13 +307,13 @@ export class SearchEmbeddable if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); - searchSource.setField('size', getServices().uiSettings.get(SAMPLE_SIZE_SETTING)); + searchSource.setField('size', this.services.uiSettings.get(SAMPLE_SIZE_SETTING)); searchSource.setField( 'sort', getSortForSearchSource( this.searchScope.sort, this.searchScope.indexPattern, - getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ) ); diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index f61fa361f0c0e..d85476568201f 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -103,6 +103,7 @@ export class SearchEmbeddableFactory filterManager, editable: getServices().capabilities.discover.save as boolean, indexPatterns: indexPattern ? [indexPattern] : [], + services: getServices(), }, input, executeTriggerActions, diff --git a/src/plugins/discover/public/application/embeddable/search_template.html b/src/plugins/discover/public/application/embeddable/search_template.html index e188d230ea307..be2f5cceac080 100644 --- a/src/plugins/discover/public/application/embeddable/search_template.html +++ b/src/plugins/discover/public/application/embeddable/search_template.html @@ -1,20 +1,20 @@ diff --git a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html new file mode 100644 index 0000000000000..6524783897f8f --- /dev/null +++ b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html @@ -0,0 +1,19 @@ + diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index 4dec1f75ba322..2ab1b93d6c37e 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -51,7 +51,7 @@ describe('getSharingData', () => { "searchRequest": Object { "body": Object { "_source": Object {}, - "fields": undefined, + "fields": Array [], "query": Object { "bool": Object { "filter": Array [], @@ -68,7 +68,9 @@ describe('getSharingData', () => { }, }, ], - "stored_fields": undefined, + "stored_fields": Array [ + "*", + ], }, "index": "the-index-pattern-title", }, diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts index 8e956eff598f3..8ec2012b5843e 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts @@ -53,6 +53,9 @@ export async function persistSavedSearch( savedSearch.columns = state.columns || []; savedSearch.sort = (state.sort as SortOrder[]) || []; + if (state.grid) { + savedSearch.grid = state.grid; + } try { const id = await savedSearch.save(saveOptions); diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 651a26cad755d..c32cf3023a25e 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -41,6 +41,7 @@ import { createTableRowDirective } from './application/angular/doc_table/compone import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; import { createDocViewerDirective } from './application/angular/doc_viewer'; +import { createDiscoverGridDirective } from './application/components/create_discover_grid_directive'; import { createRenderCompleteDirective } from './application/angular/directives/render_complete'; import { initAngularBootstrap, @@ -49,12 +50,12 @@ import { PromiseServiceCreator, registerListenEventListener, watchMultiDecorator, - createTopNavDirective, - createTopNavHelper, } from '../../kibana_legacy/public'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive'; +import { createDiscoverDirective } from './application/components/create_discover_directive'; + /** * returns the main inner angular module, it contains all the parts of Angular Discover * needs to render, so in the end the current 'kibana' angular module is no longer necessary @@ -95,7 +96,6 @@ export function initializeInnerAngularModule( createLocalI18nModule(); createLocalPrivateModule(); createLocalPromiseModule(); - createLocalTopNavModule(navigation); createLocalStorageModule(); createPagerFactoryModule(); createDocTableModule(); @@ -128,7 +128,6 @@ export function initializeInnerAngularModule( 'discoverI18n', 'discoverPrivate', 'discoverPromise', - 'discoverTopNav', 'discoverLocalStorageProvider', 'discoverDocTable', 'discoverPagerFactory', @@ -136,7 +135,8 @@ export function initializeInnerAngularModule( .config(watchMultiDecorator) .run(registerListenEventListener) .directive('renderComplete', createRenderCompleteDirective) - .directive('discoverLegacy', createDiscoverLegacyDirective); + .directive('discoverLegacy', createDiscoverLegacyDirective) + .directive('discover', createDiscoverDirective); } function createLocalPromiseModule() { @@ -147,13 +147,6 @@ function createLocalPrivateModule() { angular.module('discoverPrivate', []).provider('Private', PrivateProvider); } -function createLocalTopNavModule(navigation: NavigationStart) { - angular - .module('discoverTopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); -} - function createLocalI18nModule() { angular .module('discoverI18n', []) @@ -188,6 +181,7 @@ function createDocTableModule() { .directive('kbnTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('kbnInfiniteScroll', createInfiniteScrollDirective) + .directive('discoverGrid', createDiscoverGridDirective) .directive('docViewer', createDocViewerDirective) .directive('contextAppLegacy', createContextAppLegacy); } diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index 1ec4549f05d49..8a0ec128b4eb2 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -26,6 +26,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { description: 'text', hits: 'integer', columns: 'keyword', + grid: 'object', sort: 'keyword', version: 'integer', }; @@ -45,6 +46,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { description: 'text', hits: 'integer', columns: 'keyword', + grid: 'object', sort: 'keyword', version: 'integer', }, diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index d5e5dd765a364..7f6f1a2553d5e 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -19,6 +19,7 @@ import { SearchSource } from '../../../data/public'; import { SavedObjectSaveOpts } from '../../../saved_objects/public'; +import { DiscoverGridSettings } from '../application/components/discover_grid/types'; export type SortOrder = [string, string]; export interface SavedSearch { @@ -28,6 +29,7 @@ export interface SavedSearch { description?: string; columns: string[]; sort: SortOrder[]; + grid: DiscoverGridSettings; destroy: () => void; save: (saveOptions: SavedObjectSaveOpts) => Promise; lastSavedTitle?: string; diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index a6e42f956a025..d124a24b120fd 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -53,6 +53,7 @@ export const searchSavedObjectType: SavedObjectsType = { }, sort: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, + grid: { type: 'object', enabled: false }, version: { type: 'integer' }, }, }, diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index f45281ee62202..425928385e64a 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -33,6 +33,7 @@ import { CONTEXT_DEFAULT_SIZE_SETTING, CONTEXT_STEP_SETTING, CONTEXT_TIE_BREAKER_FIELDS_SETTING, + DOC_TABLE_LEGACY, MODIFY_COLUMNS_ON_SWITCH, } from '../common'; @@ -165,6 +166,23 @@ export const uiSettings: Record = { category: ['discover'], schema: schema.arrayOf(schema.string()), }, + [DOC_TABLE_LEGACY]: { + name: i18n.translate('discover.advancedSettings.docTableVersionName', { + defaultMessage: 'Use legacy table', + }), + value: true, + description: i18n.translate('discover.advancedSettings.docTableVersionDescription', { + defaultMessage: + 'Discover uses a new table layout that includes better data sorting, drag-and-drop columns, and a full screen ' + + 'view. Enable this option if you prefer to fall back to the legacy table.', + }), + category: ['discover'], + schema: schema.boolean(), + metric: { + type: METRIC_TYPE.CLICK, + name: 'discover:useLegacyDataGrid', + }, + }, [MODIFY_COLUMNS_ON_SWITCH]: { name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', { defaultMessage: 'Modify columns when changing index patterns', diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index d893724f616d2..8366d81a65754 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -55,6 +55,11 @@ export type EmbeddableInput = { * Search session id to group searches */ searchSessionId?: string; + + /** + * Flag whether colors should be synced with other panels + */ + syncColors?: boolean; }; export interface PanelState { diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index efaff42c19e2f..7898fac16fd9e 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -18,44 +18,13 @@ */ import { UiActionsSetup } from '../../ui_actions/public'; import { - ACTION_ADD_PANEL, - ACTION_CUSTOMIZE_PANEL, - ACTION_EDIT_PANEL, - ACTION_INSPECT_PANEL, - CONTEXT_MENU_TRIGGER, contextMenuTrigger, - EmbeddableContext, - PANEL_BADGE_TRIGGER, - PANEL_NOTIFICATION_TRIGGER, panelBadgeTrigger, panelNotificationTrigger, - RangeSelectContext, - REMOVE_PANEL_ACTION, - SELECT_RANGE_TRIGGER, selectRangeTrigger, - ValueClickContext, - VALUE_CLICK_TRIGGER, valueClickTrigger, } from './lib'; -declare module '../../ui_actions/public' { - export interface TriggerContextMapping { - [CONTEXT_MENU_TRIGGER]: EmbeddableContext; - [PANEL_BADGE_TRIGGER]: EmbeddableContext; - [PANEL_NOTIFICATION_TRIGGER]: EmbeddableContext; - [SELECT_RANGE_TRIGGER]: RangeSelectContext; - [VALUE_CLICK_TRIGGER]: ValueClickContext; - } - - export interface ActionContextMapping { - [ACTION_CUSTOMIZE_PANEL]: EmbeddableContext; - [ACTION_ADD_PANEL]: EmbeddableContext; - [ACTION_INSPECT_PANEL]: EmbeddableContext; - [REMOVE_PANEL_ACTION]: EmbeddableContext; - [ACTION_EDIT_PANEL]: EmbeddableContext; - } -} - /** * This method initializes Embeddable plugin with initial set of * triggers and actions. diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 2173082d67d3e..f776e41a20458 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -25,7 +25,6 @@ import { RenderCompleteDispatcher } from '../../../../kibana_utils/public'; import { Adapters } from '../types'; import { IContainer } from '../containers'; import { EmbeddableOutput, IEmbeddable } from './i_embeddable'; -import { TriggerContextMapping } from '../ui_actions'; import { EmbeddableInput, ViewMode } from '../../../common/types'; function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { @@ -248,7 +247,7 @@ export abstract class Embeddable< this.onResetInput(newInput); } - public supportedTriggers(): Array { + public supportedTriggers(): string[] { return []; } } diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx index bcd9d31dade26..1b18d588d8f82 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx @@ -24,7 +24,7 @@ import { HelloWorldEmbeddable, HelloWorldEmbeddableFactoryDefinition, HELLO_WORLD_EMBEDDABLE, -} from '../../../../../../examples/embeddable_examples/public/hello_world'; +} from '../../tests/fixtures'; import { EmbeddableRenderer } from './embeddable_renderer'; import { embeddablePluginMock } from '../../mocks'; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx index cb900884fde97..fa1515b27c53d 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { HelloWorldEmbeddable } from '../../../../../../examples/embeddable_examples/public'; +import { HelloWorldEmbeddable } from '../../tests/fixtures'; import { EmbeddableRoot } from './embeddable_root'; import { mount } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index a19ec2345db8d..3b0c60f3fb08e 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -20,7 +20,6 @@ import { Observable } from 'rxjs'; import { Adapters } from '../types'; import { IContainer } from '../containers/i_container'; -import { TriggerContextMapping } from '../../../../ui_actions/public'; import { EmbeddableInput } from '../../../common/types'; export interface EmbeddableError { @@ -173,5 +172,5 @@ export interface IEmbeddable< /** * List of triggers that this embeddable will execute. */ - supportedTriggers(): Array; + supportedTriggers(): string[]; } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 2104d93da9ad8..80238b06ff9fa 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -24,7 +24,7 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; -import { Action, UiActionsStart, ActionType } from '../../../../ui_actions/public'; +import { Action, UiActionsStart } from '../../../../ui_actions/public'; import { Trigger, ViewMode } from '../types'; import { isErrorEmbeddable } from '../embeddables'; import { EmbeddablePanel } from './embeddable_panel'; @@ -216,7 +216,7 @@ const renderInEditModeAndOpenContextMenu = async ( test('HelloWorldContainer in edit mode hides disabledActions', async () => { const action = { id: 'FOO', - type: 'FOO' as ActionType, + type: 'FOO', getIconType: () => undefined, getDisplayName: () => 'foo', isCompatible: async () => true, @@ -252,7 +252,7 @@ test('HelloWorldContainer in edit mode hides disabledActions', async () => { test('HelloWorldContainer hides disabled badges', async () => { const action = { id: 'BAR', - type: 'BAR' as ActionType, + type: 'BAR', getIconType: () => undefined, getDisplayName: () => 'bar', isCompatible: async () => true, diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 867092b78ef7a..3363f556b418e 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -17,20 +17,20 @@ * under the License. */ import React from 'react'; -import { NotificationsStart, OverlayStart } from 'src/core/public'; +import { NotificationsStart, OverlayRef, OverlayStart } from 'src/core/public'; import { EmbeddableStart } from '../../../../../plugin'; import { toMountPoint } from '../../../../../../../kibana_react/public'; import { IContainer } from '../../../../containers'; import { AddPanelFlyout } from './add_panel_flyout'; -export async function openAddPanelFlyout(options: { +export function openAddPanelFlyout(options: { embeddable: IContainer; getFactory: EmbeddableStart['getEmbeddableFactory']; getAllFactories: EmbeddableStart['getEmbeddableFactories']; overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; -}) { +}): OverlayRef { const { embeddable, getFactory, @@ -59,4 +59,5 @@ export async function openAddPanelFlyout(options: { ownFocus: true, } ); + return flyoutSession; } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts index ae9645767b267..5b8607ed38c00 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts @@ -55,9 +55,18 @@ export class InspectPanelAction implements Action { if (!(await this.isCompatible({ embeddable })) || adapters === undefined) { throw new Error('Action not compatible with context'); } - const session = this.inspector.open(adapters, { title: embeddable.getTitle(), + options: { + fileName: + embeddable.getTitle() || // pick the visible title + embeddable.getInput().title || // or the custom title if used, but currently hidden + embeddable.getOutput().defaultTitle || // or the saved title + // in the very last resort use "untitled" + i18n.translate('embeddableApi.panel.inspectPanel.untitledEmbeddableFilename', { + defaultMessage: 'untitled', + }), + }, }); // Overwrite the embeddables.destroy() function to close the inspector // before calling the original destroy method diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts b/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts index bb34b474efda0..ce13c40f08d7e 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts +++ b/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts @@ -17,16 +17,15 @@ * under the License. */ -import { createAction, ActionType } from '../../ui_actions'; +import { createAction } from '../../ui_actions'; import { ViewMode } from '../../types'; import { IEmbeddable } from '../..'; -// Casting to ActionType is a hack - in a real situation use -// declare module and add this id to ActionContextMapping. -export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION' as ActionType; +export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION'; export function createEditModeAction() { - return createAction({ + return createAction({ + id: EDIT_MODE_ACTION, type: EDIT_MODE_ACTION, getDisplayName: () => 'I only show up in edit mode', isCompatible: async (context: { embeddable: IEmbeddable }) => diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx index 968caf67b1826..2aa42cbc24554 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx @@ -17,12 +17,10 @@ * under the License. */ -import { IncompatibleActionError, ActionType, ActionDefinitionByType } from '../../ui_actions'; +import { IncompatibleActionError, Action } from '../../ui_actions'; import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables'; -// Casting to ActionType is a hack - in a real situation use -// declare module and add this id to ActionContextMapping. -export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION' as ActionType; +export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION'; export interface FullNameEmbeddableOutput extends EmbeddableOutput { fullName: string; @@ -42,7 +40,7 @@ export interface SayHelloActionContext { message?: string; } -export class SayHelloAction implements ActionDefinitionByType { +export class SayHelloAction implements Action { public readonly type = SAY_HELLO_ACTION; public readonly id = SAY_HELLO_ACTION; diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx b/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx index 04898550532df..1427a8dcb736e 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx @@ -18,16 +18,14 @@ */ import React from 'react'; import { EuiFlyoutBody } from '@elastic/eui'; -import { createAction, IncompatibleActionError, ActionType } from '../../ui_actions'; +import { createAction, IncompatibleActionError } from '../../ui_actions'; import { CoreStart } from '../../../../../../core/public'; import { toMountPoint } from '../../../../../kibana_react/public'; import { Embeddable, EmbeddableInput } from '../../embeddables'; import { GetMessageModal } from './get_message_modal'; import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action'; -// Casting to ActionType is a hack - in a real situation use -// declare module and add this id to ActionContextMapping. -export const ACTION_SEND_MESSAGE = 'ACTION_SEND_MESSAGE' as ActionType; +export const ACTION_SEND_MESSAGE = 'ACTION_SEND_MESSAGE'; interface ActionContext { embeddable: Embeddable; @@ -44,7 +42,8 @@ export function createSendMessageAction(overlays: CoreStart['overlays']) { overlays.openFlyout(toMountPoint({content})); }; - return createAction({ + return createAction({ + id: ACTION_SEND_MESSAGE, type: ACTION_SEND_MESSAGE, getDisplayName: () => 'Send message', isCompatible, diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx index 01228c778754b..bacc764469a78 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx @@ -24,13 +24,6 @@ import { EuiButton } from '@elastic/eui'; import * as Rx from 'rxjs'; import { UiActionsStart } from '../../../../../../ui_actions/public'; import { ContactCardEmbeddable, CONTACT_USER_TRIGGER } from './contact_card_embeddable'; -import { EmbeddableContext } from '../../../triggers'; - -declare module '../../../../../../ui_actions/public' { - export interface TriggerContextMapping { - [CONTACT_USER_TRIGGER]: EmbeddableContext; - } -} interface Props { embeddable: ContactCardEmbeddable; diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index d9fb063a5bb56..cb315b8804484 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -56,7 +56,7 @@ export type ChartActionContext = | RowClickContext; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; -export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { +export const contextMenuTrigger: Trigger = { id: CONTEXT_MENU_TRIGGER, title: i18n.translate('embeddableApi.contextMenuTrigger.title', { defaultMessage: 'Context menu', @@ -67,7 +67,7 @@ export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { }; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; -export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { +export const panelBadgeTrigger: Trigger = { id: PANEL_BADGE_TRIGGER, title: i18n.translate('embeddableApi.panelBadgeTrigger.title', { defaultMessage: 'Panel badges', @@ -78,7 +78,7 @@ export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { }; export const PANEL_NOTIFICATION_TRIGGER = 'PANEL_NOTIFICATION_TRIGGER'; -export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = { +export const panelNotificationTrigger: Trigger = { id: PANEL_NOTIFICATION_TRIGGER, title: i18n.translate('embeddableApi.panelNotificationTrigger.title', { defaultMessage: 'Panel notifications', @@ -89,7 +89,7 @@ export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = { }; export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; -export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = { +export const selectRangeTrigger: Trigger = { id: SELECT_RANGE_TRIGGER, title: i18n.translate('embeddableApi.selectRangeTrigger.title', { defaultMessage: 'Range selection', @@ -100,7 +100,7 @@ export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = { }; export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; -export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = { +export const valueClickTrigger: Trigger = { id: VALUE_CLICK_TRIGGER, title: i18n.translate('embeddableApi.valueClickTrigger.title', { defaultMessage: 'Single click', diff --git a/src/plugins/embeddable/public/plugin.test.ts b/src/plugins/embeddable/public/plugin.test.ts index 5d47463344434..757a1989d8fc7 100644 --- a/src/plugins/embeddable/public/plugin.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -20,7 +20,7 @@ import { coreMock } from '../../../core/public/mocks'; import { testPlugin } from './tests/test_plugin'; import { EmbeddableFactoryProvider } from './types'; import { defaultEmbeddableFactoryProvider } from './lib'; -import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/public'; +import { HelloWorldEmbeddable } from './tests/fixtures'; test('can set custom embeddable factory provider', async () => { const coreSetup = coreMock.createSetup(); diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index a401795c498b3..386f1b369bef8 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -34,6 +34,7 @@ import { MaybePromise } from '@kbn/utility-types'; import { NotificationsStart as NotificationsStart_2 } from 'src/core/public'; import { Observable } from 'rxjs'; import { Optional } from '@kbn/utility-types'; +import { OverlayRef as OverlayRef_2 } from 'src/core/public'; import { OverlayStart as OverlayStart_2 } from 'src/core/public'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; @@ -214,7 +215,7 @@ export const CONTEXT_MENU_TRIGGER = "CONTEXT_MENU_TRIGGER"; // Warning: (ae-missing-release-tag) "contextMenuTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'>; +export const contextMenuTrigger: Trigger; // Warning: (ae-missing-release-tag) "defaultEmbeddableFactoryProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -297,10 +298,8 @@ export abstract class Embeddable; + supportedTriggers(): string[]; // (undocumented) abstract readonly type: string; // (undocumented) @@ -410,6 +409,7 @@ export type EmbeddableInput = { disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; + syncColors?: boolean; }; // Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -661,7 +661,7 @@ export interface IEmbeddable; + supportedTriggers(): string[]; readonly type: string; updateInput(changes: Partial): void; } @@ -716,7 +716,7 @@ export function openAddPanelFlyout(options: { overlays: OverlayStart_2; notifications: NotificationsStart_2; SavedObjectFinder: React.ComponentType; -}): Promise; +}): OverlayRef_2; // Warning: (ae-missing-release-tag) "OutputSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -739,7 +739,7 @@ export const PANEL_NOTIFICATION_TRIGGER = "PANEL_NOTIFICATION_TRIGGER"; // Warning: (ae-missing-release-tag) "panelBadgeTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'>; +export const panelBadgeTrigger: Trigger; // Warning: (ae-missing-release-tag) "PanelNotFoundError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -753,7 +753,7 @@ export class PanelNotFoundError extends Error { // Warning: (ae-missing-release-tag) "panelNotificationTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'>; +export const panelNotificationTrigger: Trigger; // Warning: (ae-missing-release-tag) "PanelState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index bb3e35c949666..1eb5cbbd340a3 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -36,10 +36,7 @@ import { ERROR_EMBEDDABLE_TYPE } from '../lib/embeddables/error_embeddable'; import { FilterableEmbeddableFactory } from '../lib/test_samples/embeddables/filterable_embeddable_factory'; import { CONTACT_CARD_EMBEDDABLE } from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { SlowContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory'; -import { - HELLO_WORLD_EMBEDDABLE, - HelloWorldEmbeddableFactoryDefinition, -} from '../../../../../examples/embeddable_examples/public'; +import { HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddableFactoryDefinition } from './fixtures'; import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world_container'; import { ContactCardEmbeddableInput, diff --git a/src/plugins/embeddable/public/tests/explicit_input.test.ts b/src/plugins/embeddable/public/tests/explicit_input.test.ts index 531fbcee94db6..7ab50de40582b 100644 --- a/src/plugins/embeddable/public/tests/explicit_input.test.ts +++ b/src/plugins/embeddable/public/tests/explicit_input.test.ts @@ -27,10 +27,7 @@ import { import { FilterableEmbeddableFactory } from '../lib/test_samples/embeddables/filterable_embeddable_factory'; import { CONTACT_CARD_EMBEDDABLE } from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { SlowContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory'; -import { - HELLO_WORLD_EMBEDDABLE, - HelloWorldEmbeddableFactoryDefinition, -} from '../../../../../examples/embeddable_examples/public'; +import { HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddableFactoryDefinition } from './fixtures'; import { FilterableContainer } from '../lib/test_samples/embeddables/filterable_container'; import { isErrorEmbeddable } from '../lib'; import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world_container'; diff --git a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable.tsx b/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable.tsx new file mode 100644 index 0000000000000..55385d4125790 --- /dev/null +++ b/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable.tsx @@ -0,0 +1,54 @@ +/* + * 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 { Embeddable, EmbeddableInput, IContainer } from '../../'; + +export const HELLO_WORLD_EMBEDDABLE = 'HELLO_WORLD_EMBEDDABLE'; + +export class HelloWorldEmbeddable extends Embeddable { + // The type of this embeddable. This will be used to find the appropriate factory + // to instantiate this kind of embeddable. + public readonly type = HELLO_WORLD_EMBEDDABLE; + + constructor(initialInput: EmbeddableInput, parent?: IContainer) { + super( + // Input state is irrelevant to this embeddable, just pass it along. + initialInput, + // Initial output state - this embeddable does not do anything with output, so just + // pass along an empty object. + {}, + // Optional parent component, this embeddable can optionally be rendered inside a container. + parent + ); + } + + /** + * Render yourself at the dom node using whatever framework you like, angular, react, or just plain + * vanilla js. + * @param node + */ + public render(node: HTMLElement) { + node.innerHTML = '
HELLO WORLD!
'; + } + + /** + * This is mostly relevant for time based embeddables which need to update data + * even if EmbeddableInput has not changed at all. + */ + public reload() {} +} diff --git a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_factory.ts b/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_factory.ts new file mode 100644 index 0000000000000..5c651e254e284 --- /dev/null +++ b/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_factory.ts @@ -0,0 +1,46 @@ +/* + * 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 { IContainer, EmbeddableInput, EmbeddableFactoryDefinition, EmbeddableFactory } from '../..'; +import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable'; + +export type HelloWorldEmbeddableFactory = EmbeddableFactory; +export class HelloWorldEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition { + public readonly type = HELLO_WORLD_EMBEDDABLE; + + /** + * In our simple example, we let everyone have permissions to edit this. Most + * embeddables should check the UI Capabilities service to be sure of + * the right permissions. + */ + public async isEditable() { + return true; + } + + public async create(initialInput: EmbeddableInput, parent?: IContainer) { + return new HelloWorldEmbeddable(initialInput, parent); + } + + public getDisplayName() { + return i18n.translate('embeddableApi.helloworld.displayName', { + defaultMessage: 'hello world', + }); + } +} diff --git a/src/plugins/embeddable/public/tests/fixtures/index.ts b/src/plugins/embeddable/public/tests/fixtures/index.ts new file mode 100644 index 0000000000000..f47b4f5e5b847 --- /dev/null +++ b/src/plugins/embeddable/public/tests/fixtures/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * from './hello_world_embeddable'; +export * from './hello_world_embeddable_factory'; diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json new file mode 100644 index 0000000000000..27a887500fb68 --- /dev/null +++ b/src/plugins/embeddable/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../inspector/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../ui_actions/tsconfig.json" }, + ] +} diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__tests__/utils_string_collapsing.txt b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__fixtures__/utils_string_collapsing.txt similarity index 100% rename from src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__tests__/utils_string_collapsing.txt rename to src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__fixtures__/utils_string_collapsing.txt diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__tests__/utils_string_expanding.txt b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__fixtures__/utils_string_expanding.txt similarity index 100% rename from src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__tests__/utils_string_expanding.txt rename to src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__fixtures__/utils_string_expanding.txt diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/json_xjson_translation_tools.test.ts similarity index 92% rename from src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts rename to src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/json_xjson_translation_tools.test.ts index 8c66a87adbaa1..e13112a28868b 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/json_xjson_translation_tools/json_xjson_translation_tools.test.ts @@ -18,12 +18,12 @@ */ import _ from 'lodash'; // @ts-ignore -import collapsingTests from './utils_string_collapsing.txt'; +import collapsingTests from './__fixtures__/utils_string_collapsing.txt'; // @ts-ignore -import expandingTests from './utils_string_expanding.txt'; +import expandingTests from './__fixtures__/utils_string_expanding.txt'; -import * as utils from '../index'; -import { extractJSONStringValues } from '../parser'; +import * as utils from './index'; +import { extractJSONStringValues } from './parser'; describe('JSON to XJSON conversion tools', () => { it('will collapse multiline strings', () => { diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index fca1694747ce2..3f3cfb9ed2dd9 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -82,6 +82,7 @@ export interface IInterpreterRenderHandlers { event: (event: any) => void; hasCompatibleActions?: (event: any) => Promise; getRenderMode: () => RenderMode; + isSyncColorsEnabled: () => boolean; /** * This uiState interface is actually `PersistedState` from the visualizations plugin, * but expressions cannot know about vis or it creates a mess of circular dependencies. diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index e9e0fa18af6c3..1cf499ce2635a 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -64,6 +64,7 @@ export class ExpressionLoader { this.renderHandler = new ExpressionRenderHandler(element, { onRenderError: params && params.onRenderError, renderMode: params?.renderMode, + syncColors: params?.syncColors, hasCompatibleActions: params?.hasCompatibleActions, }); this.render$ = this.renderHandler.render$; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 404df2db019a1..5c018adc0131b 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -531,7 +531,7 @@ export interface ExpressionRenderError extends Error { // @public (undocumented) export class ExpressionRenderHandler { // Warning: (ae-forgotten-export) The symbol "ExpressionRenderHandlerParams" needs to be exported by the entry point index.d.ts - constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams); + constructor(element: HTMLElement, { onRenderError, renderMode, syncColors, hasCompatibleActions, }?: ExpressionRenderHandlerParams); // (undocumented) destroy: () => void; // (undocumented) @@ -903,6 +903,8 @@ export interface IExpressionLoaderParams { // (undocumented) searchSessionId?: string; // (undocumented) + syncColors?: boolean; + // (undocumented) uiState?: unknown; // (undocumented) variables?: Record; @@ -920,6 +922,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) hasCompatibleActions?: (event: any) => Promise; // (undocumented) + isSyncColorsEnabled: () => boolean; + // (undocumented) onDestroy: (fn: () => void) => void; // (undocumented) reload: () => void; diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index 4ebd626e70fc3..d9a4f095127b8 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -146,6 +146,35 @@ describe('ExpressionRenderer', () => { instance.unmount(); }); + it('should not update twice immediately after rendering', () => { + jest.useFakeTimers(); + + const refreshSubject = new Subject(); + const loaderUpdate = jest.fn(); + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$: new Subject(), + data$: new Subject(), + loading$: new Subject(), + update: loaderUpdate, + destroy: jest.fn(), + }; + }); + + const instance = mount( + + ); + + act(() => { + jest.runAllTimers(); + }); + + expect(loaderUpdate).toHaveBeenCalledTimes(1); + + instance.unmount(); + }); + it('waits for debounce period on other loader option change if specified', () => { jest.useFakeTimers(); @@ -304,4 +333,22 @@ describe('ExpressionRenderer', () => { expect(onEvent).toHaveBeenCalledTimes(1); expect(onEvent.mock.calls[0][0]).toBe(event); }); + + it('should correctly assign classes to the wrapper node', () => { + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$: new Subject(), + data$: new Subject(), + loading$: new Subject(), + update: jest.fn(), + destroy: jest.fn(), + }; + }); + + const instance = mount(); + // Counte is 2 because the class is applied to ReactExpressionRenderer + internal component + expect(instance.find('.myClassName').length).toBe(2); + + instance.unmount(); + }); }); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index eac2371ec66d0..caa8e209ec170 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -91,7 +91,12 @@ export const ReactExpressionRenderer = ({ ); const [debouncedExpression, setDebouncedExpression] = useState(expression); const [waitingForDebounceToComplete, setDebouncePending] = useState(false); + const firstRender = useRef(true); useShallowCompareEffect(() => { + if (firstRender.current) { + firstRender.current = false; + return; + } if (debounce === undefined) { return; } @@ -170,7 +175,12 @@ export const ReactExpressionRenderer = ({ errorRenderHandlerRef.current = null; }; - }, [hasCustomRenderErrorHandler, onEvent]); + }, [ + hasCustomRenderErrorHandler, + onEvent, + expressionLoaderOptions.renderMode, + expressionLoaderOptions.syncColors, + ]); useEffect(() => { const subscription = reload$?.subscribe(() => { @@ -206,10 +216,9 @@ export const ReactExpressionRenderer = ({ } }, [state.error]); - const classes = classNames('expExpressionRenderer', { + const classes = classNames('expExpressionRenderer', className, { 'expExpressionRenderer-isEmpty': state.isEmpty, 'expExpressionRenderer-hasError': !!state.error, - className, }); const expressionStyles: React.CSSProperties = {}; diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 717776a2861b4..e3091b908deca 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -31,6 +31,7 @@ export type IExpressionRendererExtraHandlers = Record; export interface ExpressionRenderHandlerParams { onRenderError?: RenderErrorHandlerFnType; renderMode?: RenderMode; + syncColors?: boolean; hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise; } @@ -63,6 +64,7 @@ export class ExpressionRenderHandler { { onRenderError, renderMode, + syncColors, hasCompatibleActions = async () => false, }: ExpressionRenderHandlerParams = {} ) { @@ -101,6 +103,9 @@ export class ExpressionRenderHandler { getRenderMode: () => { return renderMode || 'display'; }, + isSyncColorsEnabled: () => { + return syncColors || false; + }, hasCompatibleActions, }; } diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index f37107abbb716..d709d8ca96bbd 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -57,6 +57,7 @@ export interface IExpressionLoaderParams { onRenderError?: RenderErrorHandlerFnType; searchSessionId?: string; renderMode?: RenderMode; + syncColors?: boolean; hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; } diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 8b8678371dd83..71199560ee0c7 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -737,6 +737,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) hasCompatibleActions?: (event: any) => Promise; // (undocumented) + isSyncColorsEnabled: () => boolean; + // (undocumented) onDestroy: (fn: () => void) => void; // (undocumented) reload: () => void; diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json new file mode 100644 index 0000000000000..cce71013cefa5 --- /dev/null +++ b/src/plugins/expressions/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*", "./index.ts"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../inspector/tsconfig.json" }, + ] +} diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts index 54fed3db1de4d..58bb037f8d614 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts @@ -23,7 +23,7 @@ import { fetchProvider } from './collector_fetch'; const getMockFetchClients = (hits?: unknown[]) => { const fetchParamsMock = createCollectorFetchContextMock(); - fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: { hits: { hits } } }); return fetchParamsMock; }; diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts index 7df9b14d2efb1..ef958873d9663 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts @@ -19,6 +19,7 @@ import { get } from 'lodash'; import moment from 'moment'; +import { SearchResponse } from 'src/core/server'; import { CollectorFetchContext } from '../../../../../usage_collection/server'; interface SearchHit { @@ -41,17 +42,23 @@ export interface TelemetryResponse { last_uninstall_set: string | null; } +type ESResponse = SearchResponse; + export function fetchProvider(index: string) { - return async ({ callCluster }: CollectorFetchContext) => { - const response = await callCluster('search', { - index, - body: { - query: { term: { type: { value: 'sample-data-telemetry' } } }, - _source: { includes: ['sample-data-telemetry', 'type', 'updated_at'] }, + return async ({ esClient }: CollectorFetchContext) => { + const { body: response } = await esClient.search( + { + index, + body: { + query: { term: { type: { value: 'sample-data-telemetry' } } }, + _source: { includes: ['sample-data-telemetry', 'type', 'updated_at'] }, + }, + filter_path: 'hits.hits._id,hits.hits._source', }, - filter_path: 'hits.hits._id,hits.hits._source', - ignore: [404], - }); + { + ignore: [404], + } + ); const getLast = ( dataSet: string, diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json new file mode 100644 index 0000000000000..b2613eeecdfb0 --- /dev/null +++ b/src/plugins/home/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "config.ts", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../url_forwarding/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../telemetry/tsconfig.json" }, + ] +} diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index d3d867344a2a8..840c75fae457e 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -105,6 +105,7 @@ export class InspectorPublicPlugin implements Plugin { views={views} adapters={adapters} title={options.title} + options={options.options} dependencies={{ uiSettings: core.uiSettings }} /> ), diff --git a/src/plugins/inspector/public/types.ts b/src/plugins/inspector/public/types.ts index 63d5615fc6c6b..faccbb1f3d6b0 100644 --- a/src/plugins/inspector/public/types.ts +++ b/src/plugins/inspector/public/types.ts @@ -33,6 +33,10 @@ export interface InspectorViewProps { * The title that the inspector is currently using e.g. a visualization name. */ title: string; + /** + * A set of specific options for each view. + */ + options?: unknown; } /** @@ -61,9 +65,11 @@ export interface InspectorViewDescription { * Options that can be specified when opening the inspector. * @property {string} title - An optional title, that will be shown in the header * of the inspector. Can be used to give more context about what is being inspected. + * @property {unknown} options - A set of specific payload to be passed to inspector views */ export interface InspectorOptions { title?: string; + options?: unknown; } export type InspectorSession = OverlayRef; diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx index dbad202953b0b..fe2d96b449e8d 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -49,6 +49,7 @@ const inspectorTitle = i18n.translate('inspector.title', { interface InspectorPanelProps { adapters: Adapters; title?: string; + options?: unknown; views: InspectorViewDescription[]; dependencies: { uiSettings: IUiSettingsClient; @@ -76,6 +77,7 @@ export class InspectorPanel extends Component ); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md index 2101740983705..3e3afe88c596a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md @@ -13,7 +13,7 @@ To track a sub view inside your application (ie a flyout, a tab, form step, etc) For tracking an application view rendered using react the simplest way is to wrap your component with the `TrackApplicationView` Higher order component: kibana.json -``` +```json { "id": "myPlugin", "version": "kibana", @@ -24,27 +24,51 @@ kibana.json } ``` -Flyout component +At the application level, the application must be wrapped by the `ApplicationUsageTrackingProvider` provided in the `usageCollection`'s setup contract. + +```typescript jsx +class MyPlugin implements Plugin { + ... + public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { + const ApplicationUsageTrackingProvider = plugins.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; + + core.application.register({ + id, + title, + ..., + mount: async (params: AppMountParameters) => { + ReactDOM.render( + // Set the tracking context provider at the App level + + + + , + element + ); + return () => ReactDOM.unmountComponentAtNode(element); + }, + }); + } + ... +} ``` -import { TrackApplicationView } from 'src/plugins/usage_collection/public'; -... +Then, for every component inside the app that requires tracking the time it is on screen, and the number of general clicks: +```typescript jsx +import { TrackApplicationView } from 'src/plugins/usage_collection/public'; -render() { +const MyTrackedComponent = () => { return ( - - + + ) } ``` -Application Usage will automatically track the active minutes on screen and clicks for both the application and the `MyFlyout` component whenever the component is mounted on the screen. Application Usage pauses counting screen minutes whenever the user is tabbed to another browser window. +Application Usage will automatically track the active minutes on screen and clicks for both the application and the `MyComponent` component whenever it is mounted on the screen. Application Usage pauses counting screen minutes whenever the user is tabbed to another browser window. -The prop `viewId` is used as a unique identifier for your plugin. `applicationUsageTracker` can be passed directly from `usageCollection` setup or start contracts of the plugin. The Application Id is automatically attached to the tracked usage. +The prop `viewId` is used as a unique identifier for your plugin. The Application Id is automatically attached to the tracked usage, based on the ID used when registering your app via `core.application.register`. #### Advanced Usage diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts index 48e9068eeda7a..6be6214bae2f5 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts @@ -96,16 +96,27 @@ export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedO })), { overwrite: true } ); - await Promise.all( + const promiseStatuses = await Promise.allSettled( rawApplicationUsageTransactional.map( ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_TRANSACTIONAL_TYPE, id) // There is no bulkDelete :( ) ); + const rejectedPromises = promiseStatuses.filter( + (settledResult): settledResult is PromiseRejectedResult => + settledResult.status === 'rejected' + ); + if (rejectedPromises.length > 0) { + throw new Error( + `Failed to delete some items in ${SAVED_OBJECTS_TRANSACTIONAL_TYPE}: ${JSON.stringify( + rejectedPromises.map(({ reason }) => reason) + )}` + ); + } } } while (toCreate.size > 0); } catch (err) { - logger.warn(`Failed to rollup transactional to daily entries`); - logger.warn(err); + logger.debug(`Failed to rollup transactional to daily entries`); + logger.debug(err); } } @@ -226,7 +237,7 @@ export async function rollTotals(logger: Logger, savedObjectsClient?: ISavedObje ), ]); } catch (err) { - logger.warn(`Failed to rollup daily entries to totals`); - logger.warn(err); + logger.debug(`Failed to rollup daily entries to totals`); + logger.debug(err); } } diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json new file mode 100644 index 0000000000000..ba3661666631a --- /dev/null +++ b/src/plugins/management/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../home/tsconfig.json"}, + { "path": "../kibana_react/tsconfig.json"}, + { "path": "../kibana_utils/tsconfig.json"} + ] +} diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index 1499b3de446b5..9d4586ebce53b 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -6,5 +6,5 @@ "ui": true, "server": true, "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaReact", "charts"] + "requiredBundles": ["kibanaReact", "visDefaultEditor"] } diff --git a/src/plugins/maps_legacy/public/components/wms_internal_options.tsx b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx index d1def8153d1a8..86c15f10ae55d 100644 --- a/src/plugins/maps_legacy/public/components/wms_internal_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TextInputOption } from '../../../charts/public'; +import { TextInputOption } from '../../../vis_default_editor/public'; import { WMSOptions } from '../common/types/external_basemap_types'; interface WmsInternalOptions { diff --git a/src/plugins/maps_legacy/public/components/wms_options.tsx b/src/plugins/maps_legacy/public/components/wms_options.tsx index 4892463bb9f85..79e08478f2155 100644 --- a/src/plugins/maps_legacy/public/components/wms_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_options.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { TmsLayer } from '../index'; import { Vis } from '../../../visualizations/public'; import { RegionMapVisParams } from '../common/types/region_map_types'; -import { SelectOption, SwitchOption } from '../../../charts/public'; +import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; import { WmsInternalOptions } from './wms_internal_options'; import { WMSOptions, TileMapVisParams } from '../common/types/external_basemap_types'; diff --git a/src/plugins/navigation/public/index.ts b/src/plugins/navigation/public/index.ts index 5afc91c4445e8..a1b72eac756d3 100644 --- a/src/plugins/navigation/public/index.ts +++ b/src/plugins/navigation/public/index.ts @@ -24,7 +24,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new NavigationPublicPlugin(initializerContext); } -export { TopNavMenuData, TopNavMenu } from './top_nav_menu'; +export { TopNavMenuData, TopNavMenu, TopNavMenuProps } from './top_nav_menu'; export { NavigationPublicPluginSetup, NavigationPublicPluginStart } from './types'; diff --git a/src/plugins/navigation/tsconfig.json b/src/plugins/navigation/tsconfig.json new file mode 100644 index 0000000000000..07cfe10d7d81f --- /dev/null +++ b/src/plugins/navigation/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + ] +} diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json index e679baf6d6f06..4815deba6441d 100644 --- a/src/plugins/region_map/kibana.json +++ b/src/plugins/region_map/kibana.json @@ -15,7 +15,7 @@ ], "requiredBundles": [ "kibanaUtils", - "charts", + "charts", "visDefaultEditor" ] } diff --git a/src/plugins/region_map/public/components/region_map_options.tsx b/src/plugins/region_map/public/components/region_map_options.tsx index 4d564d7347a1e..b2bb250d66ee2 100644 --- a/src/plugins/region_map/public/components/region_map_options.tsx +++ b/src/plugins/region_map/public/components/region_map_options.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; -import { NumberInputOption, SelectOption, SwitchOption } from '../../../charts/public'; +import { SelectOption, SwitchOption, NumberInputOption } from '../../../vis_default_editor/public'; import { RegionMapVisParams, WmsOptions } from '../../../maps_legacy/public'; const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ diff --git a/src/plugins/region_map/public/get_deprecation_message.tsx b/src/plugins/region_map/public/get_deprecation_message.tsx index ea5cdf42c3111..de094fa98750f 100644 --- a/src/plugins/region_map/public/get_deprecation_message.tsx +++ b/src/plugins/region_map/public/get_deprecation_message.tsx @@ -71,6 +71,7 @@ export function getDeprecationMessage(vis: Vis) { const bucketAggs = vis.data?.aggs?.byType('buckets'); if (bucketAggs?.length && bucketAggs[0].type.dslName === 'terms') { createUrlParams.termsFieldName = bucketAggs[0].getField()?.name; + createUrlParams.termsSize = bucketAggs[0].getParam('size'); } const metricAggs = vis.data?.aggs?.byType('metrics'); diff --git a/src/plugins/region_map/public/region_map_type.js b/src/plugins/region_map/public/region_map_type.js index ec32d582ce15b..e7a339a6cc8b6 100644 --- a/src/plugins/region_map/public/region_map_type.js +++ b/src/plugins/region_map/public/region_map_type.js @@ -22,7 +22,6 @@ import { mapToLayerWithId } from './util'; import { createRegionMapVisualization } from './region_map_visualization'; import { RegionMapOptions } from './components/region_map_options'; import { truncatedColorSchemas } from '../../charts/public'; -import { Schemas } from '../../vis_default_editor/public'; import { ORIGIN } from '../../maps_legacy/public'; import { getDeprecationMessage } from './get_deprecation_message'; @@ -64,7 +63,7 @@ provided base maps, or add your own. Darker colors represent higher values.', vectorLayers: [], tmsLayers: [], }, - schemas: new Schemas([ + schemas: [ { group: 'metrics', name: 'metric', @@ -98,7 +97,7 @@ provided base maps, or add your own. Darker colors represent higher values.', max: 1, aggFilter: ['terms'], }, - ]), + ], }, setup: async (vis) => { const serviceSettings = await getServiceSettings(); diff --git a/src/plugins/runtime_fields/README.mdx b/src/plugins/runtime_fields/README.mdx new file mode 100644 index 0000000000000..15985b07caf96 --- /dev/null +++ b/src/plugins/runtime_fields/README.mdx @@ -0,0 +1,4 @@ + +# Runtime Fields + +The runtime fields plugin provides types and constants for OSS and xpack runtime field related code. diff --git a/src/plugins/runtime_fields/common/constants.ts b/src/plugins/runtime_fields/common/constants.ts new file mode 100644 index 0000000000000..568003508f4bd --- /dev/null +++ b/src/plugins/runtime_fields/common/constants.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 const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; diff --git a/src/core/server/http/prototype_pollution/index.ts b/src/plugins/runtime_fields/common/index.ts similarity index 93% rename from src/core/server/http/prototype_pollution/index.ts rename to src/plugins/runtime_fields/common/index.ts index e1a33ffba155e..b08ac661a4bd6 100644 --- a/src/core/server/http/prototype_pollution/index.ts +++ b/src/plugins/runtime_fields/common/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { validateObject } from './validate_object'; +export * from './constants'; +export * from './types'; diff --git a/src/plugins/runtime_fields/common/types.ts b/src/plugins/runtime_fields/common/types.ts new file mode 100644 index 0000000000000..f16d3d75d6ecf --- /dev/null +++ b/src/plugins/runtime_fields/common/types.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 { RUNTIME_FIELD_TYPES } from './constants'; + +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +export interface RuntimeField { + name: string; + type: RuntimeType; + script: { + source: string; + }; +} diff --git a/src/plugins/runtime_fields/kibana.json b/src/plugins/runtime_fields/kibana.json new file mode 100644 index 0000000000000..e71116f81532e --- /dev/null +++ b/src/plugins/runtime_fields/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "runtimeFields", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/runtime_fields/public/index.ts b/src/plugins/runtime_fields/public/index.ts new file mode 100644 index 0000000000000..a7a94b07ac6e8 --- /dev/null +++ b/src/plugins/runtime_fields/public/index.ts @@ -0,0 +1,28 @@ +/* + * 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 * from '../common'; + +export function plugin() { + return { + setup() {}, + start() {}, + stop() {}, + }; +} diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json new file mode 100644 index 0000000000000..d9045b91b9dfa --- /dev/null +++ b/src/plugins/saved_objects/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + ] +} diff --git a/src/plugins/saved_objects_management/public/lib/import_file.ts b/src/plugins/saved_objects_management/public/lib/import_file.ts index 84177bda3eb43..bc15954cc2c20 100644 --- a/src/plugins/saved_objects_management/public/lib/import_file.ts +++ b/src/plugins/saved_objects_management/public/lib/import_file.ts @@ -17,13 +17,13 @@ * under the License. */ -import { HttpStart, SavedObjectsImportError } from 'src/core/public'; +import { HttpStart, SavedObjectsImportFailure } from 'src/core/public'; import { ImportMode } from '../management_section/objects_table/components/import_mode_control'; interface ImportResponse { success: boolean; successCount: number; - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; } export async function importFile( diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index bb7492bb9b3de..bfa376b176f8d 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts @@ -24,12 +24,12 @@ import { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportSuccess, } from 'src/core/public'; export interface FailedImport { - obj: Omit; + obj: Omit; error: | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError @@ -40,7 +40,7 @@ export interface FailedImport { interface UnmatchedReference { existingIndexPatternId: string; - list: Array>; + list: Array>; newIndexPatternId?: string; } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 2e262ce43731a..518b1831abda8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -1,200 +1,62 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SavedObjectsTable delete should show a confirm modal 1`] = ` - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" + + selectedObjects={ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + Object { + "id": "3", + "type": "dashboard", + }, + ] } -> -

- -

- -
+/> `; exports[`SavedObjectsTable export should allow the user to choose when exporting all 1`] = ` - - - - - - - - - } - labelType="legend" - > - - - - - } - name="includeReferencesDeep" - onChange={[Function]} - /> - - - - - - - - - - - - - - - - - - - - + `; exports[`SavedObjectsTable should render normally 1`] = ` diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx new file mode 100644 index 0000000000000..db1f83759fad5 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 { mountWithIntl } from '@kbn/test/jest'; +import { SavedObjectWithMetadata } from '../../../../common'; +import { DeleteConfirmModal } from './delete_confirm_modal'; + +const createObject = (): SavedObjectWithMetadata => ({ + id: 'foo', + type: 'bar', + attributes: {}, + references: [], + meta: {}, +}); + +describe('DeleteConfirmModal', () => { + let onConfirm: jest.Mock; + let onCancel: jest.Mock; + + beforeEach(() => { + onConfirm = jest.fn(); + onCancel = jest.fn(); + }); + + it('displays a loader if `isDeleting` is true', () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('EuiLoadingElastic')).toHaveLength(1); + expect(wrapper.find('EuiModal')).toHaveLength(0); + }); + + it('lists the objects to delete', () => { + const objs = [createObject(), createObject(), createObject()]; + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('.euiTableRow')).toHaveLength(3); + }); + + it('calls `onCancel` when clicking on the cancel button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButtonEmpty').simulate('click'); + + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onConfirm).not.toHaveBeenCalled(); + }); + + it('calls `onDelete` when clicking on the delete button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButton').simulate('click'); + + expect(onConfirm).toHaveBeenCalledTimes(1); + expect(onCancel).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx new file mode 100644 index 0000000000000..07564843e9745 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.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, { FC } from 'react'; +import { + EuiInMemoryTable, + EuiLoadingElastic, + EuiToolTip, + EuiIcon, + EuiOverlayMask, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SavedObjectWithMetadata } from '../../../../common'; +import { getSavedObjectLabel } from '../../../lib'; + +export interface DeleteConfirmModalProps { + isDeleting: boolean; + onConfirm: () => void; + onCancel: () => void; + selectedObjects: SavedObjectWithMetadata[]; +} + +export const DeleteConfirmModal: FC = ({ + isDeleting, + onConfirm, + onCancel, + selectedObjects, +}) => { + if (isDeleting) { + return ( + + + + ); + } + + // can't use `EuiConfirmModal` here as the confirm modal body is wrapped + // inside a `

` element, causing UI glitches with the table. + return ( + + + + + + + + +

+ +

+ + ( + + + + ), + }, + { + field: 'id', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', + { defaultMessage: 'Id' } + ), + }, + { + field: 'meta.title', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', + { defaultMessage: 'Title' } + ), + }, + ]} + pagination={true} + sorting={false} + /> + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.test.tsx new file mode 100644 index 0000000000000..c76c5b68cd66f --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { mountWithIntl } from '@kbn/test/jest'; +import { ExportModal } from './export_modal'; + +describe('ExportModal', () => { + let onExport: jest.Mock; + let onCancel: jest.Mock; + let onSelectedOptionsChange: jest.Mock; + let onIncludeReferenceChange: jest.Mock; + + const options = [ + { id: '1', label: 'option 1' }, + { id: '2', label: 'option 2' }, + ]; + const selectedOptions = { + 1: true, + 2: false, + }; + + beforeEach(() => { + onExport = jest.fn(); + onCancel = jest.fn(); + onSelectedOptionsChange = jest.fn(); + onIncludeReferenceChange = jest.fn(); + }); + + it('Displays a checkbox for each option', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('EuiCheckbox')).toHaveLength(2); + }); + + it('calls `onCancel` when clicking on the cancel button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButtonEmpty').simulate('click'); + + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onExport).not.toHaveBeenCalled(); + }); + + it('calls `onExport` when clicking on the export button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButton').simulate('click'); + + expect(onExport).toHaveBeenCalledTimes(1); + expect(onCancel).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx new file mode 100644 index 0000000000000..01ef145bcd077 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx @@ -0,0 +1,137 @@ +/* + * 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, { FC } from 'react'; +import { + EuiOverlayMask, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiSpacer, + EuiFormRow, + EuiCheckboxGroup, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ExportModalProps { + onExport: () => void; + onCancel: () => void; + onSelectedOptionsChange: (newSelectedOptions: Record) => void; + filteredItemCount: number; + options: Array<{ id: string; label: string }>; + selectedOptions: Record; + includeReferences: boolean; + onIncludeReferenceChange: (newIncludeReference: boolean) => void; +} + +export const ExportModal: FC = ({ + onCancel, + onExport, + onSelectedOptionsChange, + options, + filteredItemCount, + selectedOptions, + includeReferences, + onIncludeReferenceChange, +}) => { + return ( + + + + + + + + + + } + labelType="legend" + > + { + onSelectedOptionsChange({ + ...selectedOptions, + ...{ + [optionId]: !selectedOptions[optionId], + }, + }); + }} + /> + + + + } + checked={includeReferences} + onChange={() => onIncludeReferenceChange(!includeReferences)} + /> + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts index 9c8736a9011eb..23e681b92b269 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts @@ -21,3 +21,5 @@ export { Header } from './header'; export { Table } from './table'; export { Flyout } from './flyout'; export { Relationships } from './relationships'; +export { DeleteConfirmModal } from './delete_confirm_modal'; +export { ExportModal } from './export_modal'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 0171c13fd974b..1991a60018aa0 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -325,7 +325,7 @@ describe('SavedObjectsTable', () => { (component.find('Header') as any).prop('onExportAll')(); component.update(); - expect(component.find('EuiModal')).toMatchSnapshot(); + expect(component.find('ExportModal')).toMatchSnapshot(); }); it('should export all', async () => { @@ -504,7 +504,7 @@ describe('SavedObjectsTable', () => { await component.instance().onDelete(); component.update(); - expect(component.find('EuiConfirmModal')).toMatchSnapshot(); + expect(component.find('DeleteConfirmModal')).toMatchSnapshot(); }); it('should delete selected objects', async () => { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index a5a4bcab364af..bb158b7621125 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -21,32 +21,8 @@ import React, { Component } from 'react'; import { debounce } from 'lodash'; // @ts-expect-error import { saveAs } from '@elastic/filesaver'; -import { - EuiSpacer, - Query, - EuiInMemoryTable, - EuiIcon, - EuiConfirmModal, - EuiLoadingElastic, - EuiOverlayMask, - EUI_MODAL_CONFIRM_BUTTON, - EuiCheckboxGroup, - EuiToolTip, - EuiPageContent, - EuiSwitch, - EuiModal, - EuiModalHeader, - EuiModalBody, - EuiModalFooter, - EuiButtonEmpty, - EuiButton, - EuiModalHeaderTitle, - EuiFormRow, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiSpacer, Query, EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { SavedObjectsClientContract, SavedObjectsFindOptions, @@ -62,7 +38,6 @@ import { parseQuery, getSavedObjectCounts, getRelationships, - getSavedObjectLabel, fetchExportObjects, fetchExportByTypeAndSearch, findObjects, @@ -77,7 +52,14 @@ import { SavedObjectsManagementActionServiceStart, SavedObjectsManagementColumnServiceStart, } from '../../services'; -import { Header, Table, Flyout, Relationships } from './components'; +import { + Header, + Table, + Flyout, + Relationships, + DeleteConfirmModal, + ExportModal, +} from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; interface ExportAllOption { @@ -554,114 +536,24 @@ export class SavedObjectsTable extends Component; - } else { - const onCancel = () => { - this.setState({ isShowingDeleteConfirmModal: false }); - }; - - const onConfirm = () => { - this.delete(); - }; - - modal = ( - - } - onCancel={onCancel} - onConfirm={onConfirm} - buttonColor="danger" - cancelButtonText={ - - } - confirmButtonText={ - isDeleting ? ( - - ) : ( - - ) - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- -

- ( - - - - ), - }, - { - field: 'id', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', - { defaultMessage: 'Id' } - ), - }, - { - field: 'meta.title', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', - { defaultMessage: 'Title' } - ), - }, - ]} - pagination={true} - sorting={false} - /> -
- ); - } - - return {modal}; + return ( + { + this.delete(); + }} + onCancel={() => { + this.setState({ isShowingDeleteConfirmModal: false }); + }} + selectedObjects={selectedSavedObjects} + /> + ); } - changeIncludeReferencesDeep = () => { - this.setState((state) => ({ - isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked, - })); - }; - - closeExportAllModal = () => { - this.setState({ isShowingExportAllOptionsModal: false }); - }; - renderExportAllOptionsModal() { const { isShowingExportAllOptionsModal, @@ -676,85 +568,26 @@ export class SavedObjectsTable extends Component - - - - - - - - - } - labelType="legend" - > - { - const newExportAllSelectedOptions = { - ...exportAllSelectedOptions, - ...{ - [optionId]: !exportAllSelectedOptions[optionId], - }, - }; - - this.setState({ - exportAllSelectedOptions: newExportAllSelectedOptions, - }); - }} - /> - - - - } - checked={isIncludeReferencesDeepChecked} - onChange={this.changeIncludeReferencesDeep} - /> - - - - - - - - - - - - - - - - - - - - - + { + this.setState({ isShowingExportAllOptionsModal: false }); + }} + onSelectedOptionsChange={(newOptions) => { + this.setState({ + exportAllSelectedOptions: newOptions, + }); + }} + filteredItemCount={filteredItemCount} + options={exportAllOptions} + selectedOptions={exportAllSelectedOptions} + includeReferences={isIncludeReferencesDeepChecked} + onIncludeReferenceChange={(newIncludeReferences) => { + this.setState({ + isIncludeReferencesDeepChecked: newIncludeReferences, + }); + }} + /> ); } diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json new file mode 100644 index 0000000000000..b0059c71424bf --- /dev/null +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + ] +} diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index 820f2c7c4c4af..23b4b0640e978 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -31,7 +31,7 @@ import { SavedObjectsClientContract, SavedObjectsClient, CoreStart, - ILegacyCustomClusterClient, + ICustomClusterClient, } from '../../../core/server'; import { getTelemetryOptIn, @@ -65,7 +65,7 @@ export class FetcherTask { private isSending = false; private internalRepository?: SavedObjectsClientContract; private telemetryCollectionManager?: TelemetryCollectionManagerPluginStart; - private elasticsearchClient?: ILegacyCustomClusterClient; + private elasticsearchClient?: ICustomClusterClient; constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); @@ -79,7 +79,7 @@ export class FetcherTask { ) { this.internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository()); this.telemetryCollectionManager = telemetryCollectionManager; - this.elasticsearchClient = elasticsearch.legacy.createClient('telemetry-fetcher'); + this.elasticsearchClient = elasticsearch.createClient('telemetry-fetcher'); this.intervalId = timer(this.initialCheckDelayMs, this.checkIntervalMs).subscribe(() => this.sendIfDue() diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index f40c38b2cbbd0..95b44ae560f20 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -99,7 +99,7 @@ export class TelemetryPlugin implements Plugin { - const usage = await usageCollection.bulkFetch( - callWithInternalUser, - asInternalUser, - soClient, - kibanaRequest - ); + const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 12245ce62305e..866aed520bd2e 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -71,7 +71,7 @@ export const getLocalStats: StatsGetter = async ( config, context ) => { - const { callCluster, usageCollection, esClient, soClient, kibanaRequest } = config; + const { usageCollection, esClient, soClient, kibanaRequest } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -79,7 +79,7 @@ export const getLocalStats: StatsGetter = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, callCluster, esClient, soClient, kibanaRequest), + getKibana(usageCollection, esClient, soClient, kibanaRequest), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index a135f4b115b21..247d9ce2c366c 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -26,7 +26,6 @@ import { Logger, IClusterClient, SavedObjectsServiceStart, - ILegacyClusterClient, } from 'src/core/server'; import { @@ -53,7 +52,6 @@ export class TelemetryCollectionManagerPlugin private collectionStrategy: CollectionStrategy | undefined; private usageGetterMethodPriority = -1; private usageCollection?: UsageCollectionSetup; - private legacyElasticsearchClient?: ILegacyClusterClient; private elasticsearchClient?: IClusterClient; private savedObjectsService?: SavedObjectsServiceStart; private readonly isDistributable: boolean; @@ -77,7 +75,6 @@ export class TelemetryCollectionManagerPlugin } public start(core: CoreStart) { - this.legacyElasticsearchClient = core.elasticsearch.legacy.client; // TODO: Remove when all the collectors have migrated this.elasticsearchClient = core.elasticsearch.client; this.savedObjectsService = core.savedObjects; @@ -129,9 +126,6 @@ export class TelemetryCollectionManagerPlugin config: StatsGetterConfig, usageCollection: UsageCollectionSetup ): StatsCollectionConfig | undefined { - const callCluster = config.unencrypted - ? this.legacyElasticsearchClient?.asScoped(config.request).callAsCurrentUser - : this.legacyElasticsearchClient?.callAsInternalUser; // Scope the new elasticsearch Client appropriately and pass to the stats collection config const esClient = config.unencrypted ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser @@ -143,8 +137,8 @@ export class TelemetryCollectionManagerPlugin // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted const kibanaRequest = config.unencrypted ? config.request : void 0; - if (callCluster && esClient && soClient) { - return { callCluster, usageCollection, esClient, soClient, kibanaRequest }; + if (esClient && soClient) { + return { usageCollection, esClient, soClient, kibanaRequest }; } } diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 05641d5064593..49e217b9e3d75 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -18,7 +18,6 @@ */ import { - LegacyAPICaller, ElasticsearchClient, Logger, KibanaRequest, @@ -68,7 +67,6 @@ export interface ClusterDetails { export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; - callCluster: LegacyAPICaller; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract | ISavedObjectsRepository; kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter diff --git a/src/plugins/telemetry_management_section/kibana.json b/src/plugins/telemetry_management_section/kibana.json index fff1699c32f8c..dbbc2fc4ac2a4 100644 --- a/src/plugins/telemetry_management_section/kibana.json +++ b/src/plugins/telemetry_management_section/kibana.json @@ -3,8 +3,8 @@ "version": "kibana", "server": false, "ui": true, - "optionalPlugins": ["usageCollection"], "requiredBundles": ["usageCollection"], + "optionalPlugins": ["usageCollection"], "requiredPlugins": [ "advancedSettings", "telemetry" diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx index b1c9fe6238979..e7693d1e918bb 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -240,10 +240,6 @@ describe('TelemetryManagementSectionComponent', () => { it('shows the OptInSecurityExampleFlyout', () => { const onQueryMatchChange = jest.fn(); const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); - const applicationUsageTrackerMock = { - trackApplicationViewUsage: jest.fn(), - flushTrackedView: jest.fn(), - } as any; const telemetryService = new TelemetryService({ config: { enabled: true, @@ -262,7 +258,6 @@ describe('TelemetryManagementSectionComponent', () => { const component = mountWithIntl( { const toggleExampleComponent = component.find('FormattedMessage > EuiLink[onClick]').at(1); const updatedView = toggleExampleComponent.simulate('click'); updatedView.find('OptInSecurityExampleFlyout'); - expect(applicationUsageTrackerMock.trackApplicationViewUsage).toHaveBeenCalled(); - expect(applicationUsageTrackerMock.flushTrackedView).not.toHaveBeenCalled(); updatedView.simulate('close'); } finally { component.unmount(); - expect(applicationUsageTrackerMock.flushTrackedView).toHaveBeenCalled(); } }); it('does not show the endpoint link when isSecurityExampleEnabled returns false', () => { const onQueryMatchChange = jest.fn(); const isSecurityExampleEnabled = jest.fn().mockReturnValue(false); - const applicationUsageTrackerMock = { - trackApplicationViewUsage: jest.fn(), - flushTrackedView: jest.fn(), - } as any; const telemetryService = new TelemetryService({ config: { enabled: true, @@ -322,11 +310,8 @@ describe('TelemetryManagementSectionComponent', () => { const description = (component.instance() as TelemetryManagementSection).renderDescription(); expect(isSecurityExampleEnabled).toBeCalled(); expect(description).toMatchSnapshot(); - expect(applicationUsageTrackerMock.trackApplicationViewUsage).not.toHaveBeenCalled(); - expect(applicationUsageTrackerMock.flushTrackedView).not.toHaveBeenCalled(); } finally { component.unmount(); - expect(applicationUsageTrackerMock.flushTrackedView).not.toHaveBeenCalled(); } }); diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index 504376205c48f..a6c0a738d14f6 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -37,7 +37,7 @@ import { OptInExampleFlyout } from './opt_in_example_flyout'; import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout'; import { LazyField } from '../../../advanced_settings/public'; import { ToastsStart } from '../../../../core/public'; -import { TrackApplicationView, UsageCollectionSetup } from '../../../usage_collection/public'; +import { TrackApplicationView } from '../../../usage_collection/public'; type TelemetryService = TelemetryPluginSetup['telemetryService']; @@ -51,7 +51,6 @@ interface Props { enableSaving: boolean; query?: any; toasts: ToastsStart; - applicationUsageTracker?: UsageCollectionSetup['applicationUsageTracker']; } interface State { @@ -92,7 +91,7 @@ export class TelemetryManagementSection extends Component { } render() { - const { telemetryService, isSecurityExampleEnabled, applicationUsageTracker } = this.props; + const { telemetryService, isSecurityExampleEnabled } = this.props; const { showExample, showSecurityExample, queryMatches, enabled, processing } = this.state; const securityExampleEnabled = isSecurityExampleEnabled(); @@ -107,10 +106,7 @@ export class TelemetryManagementSection extends Component { return ( {showExample && ( - + { )} {showSecurityExample && securityExampleEnabled && ( - + )} diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx index 99200787c362b..9722be1e65adf 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx @@ -20,7 +20,6 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; // It should be this but the types are way too vague in the AdvancedSettings plugin `Record` // type Props = Omit; type Props = any; @@ -29,15 +28,13 @@ const TelemetryManagementSectionComponent = lazy(() => import('./telemetry_manag export function telemetryManagementSectionWrapper( telemetryService: TelemetryPluginSetup['telemetryService'], - shouldShowSecuritySolutionUsageExample: () => boolean, - applicationUsageTracker?: UsageCollectionSetup['applicationUsageTracker'] + shouldShowSecuritySolutionUsageExample: () => boolean ) { const TelemetryManagementSectionWrapper = (props: Props) => ( }> diff --git a/src/plugins/telemetry_management_section/public/plugin.tsx b/src/plugins/telemetry_management_section/public/plugin.tsx index ed026a267df36..f8220b2dacbf3 100644 --- a/src/plugins/telemetry_management_section/public/plugin.tsx +++ b/src/plugins/telemetry_management_section/public/plugin.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import React from 'react'; import { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public'; import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; @@ -55,17 +56,24 @@ export class TelemetryManagementSectionPlugin core: CoreSetup, { advancedSettings, - usageCollection, telemetry: { telemetryService }, + usageCollection, }: TelemetryManagementSectionPluginDepsSetup ) { + const ApplicationUsageTrackingProvider = + usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; advancedSettings.component.register( advancedSettings.component.componentType.PAGE_FOOTER_COMPONENT, - telemetryManagementSectionWrapper( - telemetryService, - this.shouldShowSecuritySolutionExample, - usageCollection?.applicationUsageTracker - ), + (props) => { + return ( + + {telemetryManagementSectionWrapper( + telemetryService, + this.shouldShowSecuritySolutionExample + )(props)} + + ); + }, true ); diff --git a/src/plugins/tile_map/public/components/tile_map_options.tsx b/src/plugins/tile_map/public/components/tile_map_options.tsx index 1a7b11ccf6e20..a6c0bb8a50dda 100644 --- a/src/plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/plugins/tile_map/public/components/tile_map_options.tsx @@ -21,8 +21,13 @@ import React, { useEffect } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { BasicOptions, RangeOption, SelectOption, SwitchOption } from '../../../charts/public'; +import { + VisOptionsProps, + BasicOptions, + SelectOption, + SwitchOption, + RangeOption, +} from '../../../vis_default_editor/public'; import { WmsOptions, TileMapVisParams, MapTypes } from '../../../maps_legacy/public'; export type TileMapOptionsProps = VisOptionsProps; diff --git a/src/plugins/tile_map/public/tile_map_type.js b/src/plugins/tile_map/public/tile_map_type.js index c5e3f0d578e30..3e9b5516322d9 100644 --- a/src/plugins/tile_map/public/tile_map_type.js +++ b/src/plugins/tile_map/public/tile_map_type.js @@ -20,7 +20,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { convertToGeoJson, MapTypes } from '../../maps_legacy/public'; -import { Schemas } from '../../vis_default_editor/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; import { supportsCssFilters } from './css_filters'; @@ -115,7 +114,7 @@ export function createTileMapTypeDefinition(dependencies) { tmsLayers: [], }, optionsTemplate: (props) => , - schemas: new Schemas([ + schemas: [ { group: 'metrics', name: 'metric', @@ -137,7 +136,7 @@ export function createTileMapTypeDefinition(dependencies) { min: 1, max: 1, }, - ]), + ], }, setup: async (vis) => { let tmsLayers; diff --git a/src/plugins/ui_actions/public/actions/action.test.ts b/src/plugins/ui_actions/public/actions/action.test.ts index 1f76223a0d7c4..4a51c0323fa8b 100644 --- a/src/plugins/ui_actions/public/actions/action.test.ts +++ b/src/plugins/ui_actions/public/actions/action.test.ts @@ -18,14 +18,12 @@ */ import { ActionExecutionContext, createAction } from '../../../ui_actions/public'; -import { ActionType } from '../types'; import { defaultTrigger } from '../triggers'; -const sayHelloAction = createAction({ - // Casting to ActionType is a hack - in a real situation use - // declare module and add this id to ActionContextMapping. - type: 'test' as ActionType, - isCompatible: ({ amICompatible }: { amICompatible: boolean }) => Promise.resolve(amICompatible), +const sayHelloAction = createAction<{ amICompatible: boolean }>({ + id: 'test', + type: 'test', + isCompatible: ({ amICompatible }) => Promise.resolve(amICompatible), execute: () => Promise.resolve(), }); @@ -33,7 +31,7 @@ test('action is not compatible based on context', async () => { const isCompatible = await sayHelloAction.isCompatible({ amICompatible: false, trigger: defaultTrigger, - } as ActionExecutionContext); + } as ActionExecutionContext<{ amICompatible: boolean }>); expect(isCompatible).toBe(false); }); @@ -41,6 +39,6 @@ test('action is compatible based on context', async () => { const isCompatible = await sayHelloAction.isCompatible({ amICompatible: true, trigger: defaultTrigger, - } as ActionExecutionContext); + } as ActionExecutionContext<{ amICompatible: boolean }>); expect(isCompatible).toBe(true); }); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 8005dadd8f5ef..5d479577d4bc6 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -18,15 +18,9 @@ */ import { UiComponent } from 'src/plugins/kibana_utils/public'; -import { ActionType, ActionContextMapping, BaseContext } from '../types'; import { Presentable } from '../util/presentable'; import { Trigger } from '../triggers'; -export type ActionByType = Action; -export type ActionDefinitionByType = ActionDefinition< - ActionContextMapping[T] ->; - /** * During action execution we can provide additional information, * for example, trigger, that caused the action execution @@ -41,19 +35,18 @@ export interface ActionExecutionMeta { /** * Action methods are executed with Context from trigger + {@link ActionExecutionMeta} */ -export type ActionExecutionContext = Context & - ActionExecutionMeta; +export type ActionExecutionContext = Context & ActionExecutionMeta; /** * Simplified action context for {@link ActionDefinition} * When defining action consumer may use either it's own Context * or an ActionExecutionContext to get access to {@link ActionExecutionMeta} params */ -export type ActionDefinitionContext = +export type ActionDefinitionContext = | Context | ActionExecutionContext; -export interface Action +export interface Action extends Partial>> { /** * Determined the order when there is more than one action matched to a trigger. @@ -69,7 +62,7 @@ export interface Action /** * The action type is what determines the context shape. */ - readonly type: T; + readonly type: string; /** * Optional EUI icon type that can be displayed along with the title. @@ -117,7 +110,7 @@ export interface Action /** * A convenience interface used to register an action. */ -export interface ActionDefinition +export interface ActionDefinition extends Partial>> { /** * ID of the action that uniquely identifies this action in the actions registry. @@ -127,7 +120,7 @@ export interface ActionDefinition /** * ID of the factory for this action. Used to construct dynamic actions. */ - readonly type?: ActionType; + readonly type?: string; /** * Returns a promise that resolves to true if this item is compatible given diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index fe7c986bdb7ef..7cae0f68bd004 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -22,7 +22,6 @@ import React from 'react'; import { Action, ActionContext as Context, ActionDefinition } from './action'; import { Presentable, PresentableGrouping } from '../util/presentable'; import { uiToReactComponent } from '../../../kibana_react/public'; -import { ActionType } from '../types'; /** * @internal @@ -32,7 +31,7 @@ export class ActionInternal
constructor(public readonly definition: A) {} public readonly id: string = this.definition.id; - public readonly type: ActionType = this.definition.type || ''; + public readonly type: string = this.definition.type || ''; public readonly order: number = this.definition.order || 0; public readonly MenuItem? = this.definition.MenuItem; public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index dea21678eccea..0199812c40802 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -17,25 +17,16 @@ * under the License. */ -import { ActionContextMapping } from '../types'; -import { ActionByType } from './action'; -import { ActionType } from '../types'; -import { ActionDefinition } from './action'; +import { ActionDefinition, Action } from './action'; -interface ActionDefinitionByType - extends Omit, 'id'> { - id?: string; -} - -export function createAction( - action: ActionDefinitionByType -): ActionByType { +export function createAction( + action: ActionDefinition +): Action { return { getIconType: () => undefined, order: 0, - id: action.type, isCompatible: () => Promise.resolve(true), getDisplayName: () => '', ...action, - } as ActionByType; + } as Action; } diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts index 3111a0b55084c..ac19889aa6532 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts @@ -34,6 +34,7 @@ const createTestAction = ({ grouping?: PresentableGrouping; }) => createAction({ + id: type as any, // mapping doesn't matter for this test type: type as any, // mapping doesn't matter for this test getDisplayName: () => dispayName, order, 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 63586ca3da1f7..aa3fd57be695a 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 @@ -24,7 +24,6 @@ import { i18n } from '@kbn/i18n'; import { uiToReactComponent } from '../../../kibana_react/public'; import { Action, ActionExecutionContext } from '../actions'; import { Trigger } from '../triggers'; -import { BaseContext } from '../types'; export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { defaultMessage: 'Options', @@ -34,7 +33,7 @@ export const txtMore = i18n.translate('uiActions.actionPanel.more', { defaultMessage: 'More', }); -interface ActionWithContext { +interface ActionWithContext { action: Action; context: Context; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 7890e4bab44a3..9ac5c19033e45 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -39,7 +39,6 @@ export { } from './util'; export { Trigger, - TriggerContext, VISUALIZE_FIELD_TRIGGER, visualizeFieldTrigger, VISUALIZE_GEO_FIELD_TRIGGER, @@ -49,18 +48,9 @@ export { RowClickContext, } from './triggers'; export { - TriggerContextMapping, - TriggerId, - ActionContextMapping, - ActionType, VisualizeFieldContext, ACTION_VISUALIZE_FIELD, ACTION_VISUALIZE_GEO_FIELD, ACTION_VISUALIZE_LENS_FIELD, } from './types'; -export { - ActionByType, - ActionDefinitionByType, - ActionExecutionContext, - ActionExecutionMeta, -} from './actions'; +export { ActionExecutionContext, ActionExecutionMeta } from './actions'; diff --git a/src/plugins/ui_actions/public/mocks.ts b/src/plugins/ui_actions/public/mocks.ts index 759430169b613..fa24738014910 100644 --- a/src/plugins/ui_actions/public/mocks.ts +++ b/src/plugins/ui_actions/public/mocks.ts @@ -21,7 +21,6 @@ import { CoreSetup, CoreStart } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '.'; import { plugin as pluginInitializer } from '.'; import { coreMock } from '../../../core/public/mocks'; -import { TriggerId } from './types'; export type Setup = jest.Mocked; export type Start = jest.Mocked; @@ -50,7 +49,7 @@ const createStartContract = (): Start => { getAction: jest.fn(), hasAction: jest.fn(), getTrigger: jest.fn(), - getTriggerActions: jest.fn((id: TriggerId) => []), + getTriggerActions: jest.fn((id: string) => []), getTriggerCompatibleActions: jest.fn(), registerAction: jest.fn(), registerTrigger: jest.fn(), diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 808cb1f3fbca0..0d3ab1086904d 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -15,11 +15,10 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import React from 'react'; import { UiComponent } from 'src/plugins/kibana_utils/public'; -// Warning: (ae-forgotten-export) The symbol "BaseContext" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Action" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface Action extends Partial>> { +export interface Action extends Partial>> { execute(context: ActionExecutionContext): Promise; getDisplayName(context: ActionExecutionContext): string; getHref?(context: ActionExecutionContext): Promise; @@ -31,7 +30,7 @@ export interface Action extend }>; order?: number; shouldAutoExecute?(context: ActionExecutionContext): Promise; - readonly type: T; + readonly type: string; } // Warning: (ae-missing-release-tag) "ACTION_VISUALIZE_FIELD" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -49,36 +48,10 @@ export const ACTION_VISUALIZE_GEO_FIELD = "ACTION_VISUALIZE_GEO_FIELD"; // @public (undocumented) export const ACTION_VISUALIZE_LENS_FIELD = "ACTION_VISUALIZE_LENS_FIELD"; -// Warning: (ae-missing-release-tag) "ActionByType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ActionByType = Action; - -// Warning: (ae-missing-release-tag) "ActionContextMapping" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface ActionContextMapping { - // Warning: (ae-forgotten-export) The symbol "DEFAULT_ACTION" needs to be exported by the entry point index.d.ts - // - // (undocumented) - [DEFAULT_ACTION]: BaseContext; - // (undocumented) - [ACTION_VISUALIZE_FIELD]: VisualizeFieldContext; - // (undocumented) - [ACTION_VISUALIZE_GEO_FIELD]: VisualizeFieldContext; - // (undocumented) - [ACTION_VISUALIZE_LENS_FIELD]: VisualizeFieldContext; -} - -// Warning: (ae-missing-release-tag) "ActionDefinitionByType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ActionDefinitionByType = UiActionsActionDefinition; - // Warning: (ae-missing-release-tag) "ActionExecutionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type ActionExecutionContext = Context & ActionExecutionMeta; +export type ActionExecutionContext = Context & ActionExecutionMeta; // Warning: (ae-missing-release-tag) "ActionExecutionMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -87,22 +60,16 @@ export interface ActionExecutionMeta { trigger: Trigger; } -// Warning: (ae-missing-release-tag) "ActionType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ActionType = keyof ActionContextMapping; - // Warning: (ae-forgotten-export) The symbol "BuildContextMenuParams" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "buildContextMenuForActions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function buildContextMenuForActions({ actions, title, closeMenu, }: BuildContextMenuParams): Promise; -// Warning: (ae-forgotten-export) The symbol "ActionDefinitionByType" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createAction(action: ActionDefinitionByType_2): ActionByType; +export function createAction(action: UiActionsActionDefinition): Action; // Warning: (ae-missing-release-tag) "IncompatibleActionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -142,61 +109,34 @@ export interface RowClickContext { // Warning: (ae-missing-release-tag) "rowClickTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'>; +export const rowClickTrigger: Trigger; // Warning: (ae-missing-release-tag) "Trigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface Trigger { +export interface Trigger { description?: string; - id: ID; + id: string; title?: string; } -// Warning: (ae-missing-release-tag) "TriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TriggerContext = T extends TriggerId ? TriggerContextMapping[T] : never; - -// Warning: (ae-missing-release-tag) "TriggerContextMapping" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface TriggerContextMapping { - // Warning: (ae-forgotten-export) The symbol "DEFAULT_TRIGGER" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "TriggerContext" needs to be exported by the entry point index.d.ts - // - // (undocumented) - [DEFAULT_TRIGGER]: TriggerContext_2; - // (undocumented) - [ROW_CLICK_TRIGGER]: RowClickContext; - // (undocumented) - [VISUALIZE_FIELD_TRIGGER]: VisualizeFieldContext; - // (undocumented) - [VISUALIZE_GEO_FIELD_TRIGGER]: VisualizeFieldContext; -} - -// Warning: (ae-missing-release-tag) "TriggerId" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type TriggerId = keyof TriggerContextMapping; - // Warning: (ae-forgotten-export) The symbol "ActionDefinitionContext" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ActionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface UiActionsActionDefinition extends Partial>> { +export interface UiActionsActionDefinition extends Partial>> { execute(context: ActionDefinitionContext): Promise; getHref?(context: ActionDefinitionContext): Promise; readonly id: string; isCompatible?(context: ActionDefinitionContext): Promise; shouldAutoExecute?(context: ActionDefinitionContext): Promise; - readonly type?: ActionType; + readonly type?: string; } // Warning: (ae-missing-release-tag) "Presentable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface UiActionsPresentable { +export interface UiActionsPresentable { getDisplayName(context: Context): string; getDisplayNameTooltip(context: Context): string; getHref?(context: Context): Promise; @@ -214,7 +154,7 @@ export interface UiActionsPresentable { // Warning: (ae-missing-release-tag) "PresentableGrouping" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type UiActionsPresentableGrouping = Array>; +export type UiActionsPresentableGrouping = Array>; // Warning: (ae-missing-release-tag) "UiActionsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -225,35 +165,35 @@ export class UiActionsService { // // (undocumented) protected readonly actions: ActionRegistry; - readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; + readonly addTriggerAction: (triggerId: string, action: UiActionsActionDefinition) => void; // (undocumented) - readonly attachAction: (triggerId: T, actionId: string) => void; + readonly attachAction: (triggerId: string, actionId: string) => void; readonly clear: () => void; // (undocumented) - readonly detachAction: (triggerId: TriggerId, actionId: string) => void; + readonly detachAction: (triggerId: string, actionId: string) => void; // @deprecated (undocumented) - readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise; + readonly executeTriggerActions: (triggerId: string, context: object) => Promise; // Warning: (ae-forgotten-export) The symbol "UiActionsExecutionService" needs to be exported by the entry point index.d.ts // // (undocumented) readonly executionService: UiActionsExecutionService; readonly fork: () => UiActionsService; // (undocumented) - readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel">; + readonly getAction: >(id: string) => Action>; // Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly getTrigger: (triggerId: T) => TriggerContract; + readonly getTrigger: (triggerId: string) => TriggerContract; // (undocumented) - readonly getTriggerActions: (triggerId: T) => Action[]; + readonly getTriggerActions: (triggerId: string) => Action[]; // (undocumented) - readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; + readonly getTriggerCompatibleActions: (triggerId: string, context: object) => Promise; // (undocumented) readonly hasAction: (actionId: string) => boolean; // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel">; + readonly registerAction: >(definition: A) => Action>; // (undocumented) readonly registerTrigger: (trigger: Trigger) => void; // Warning: (ae-forgotten-export) The symbol "TriggerRegistry" needs to be exported by the entry point index.d.ts @@ -314,12 +254,12 @@ export interface VisualizeFieldContext { // Warning: (ae-missing-release-tag) "visualizeFieldTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const visualizeFieldTrigger: Trigger<'VISUALIZE_FIELD_TRIGGER'>; +export const visualizeFieldTrigger: Trigger; // Warning: (ae-missing-release-tag) "visualizeGeoFieldTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const visualizeGeoFieldTrigger: Trigger<'VISUALIZE_GEO_FIELD_TRIGGER'>; +export const visualizeGeoFieldTrigger: Trigger; // Warnings were encountered during analysis: diff --git a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts index 59616dcf3f38d..ea44448092a72 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts @@ -19,14 +19,13 @@ import { uniqBy } from 'lodash'; import { Action } from '../actions'; -import { BaseContext } from '../types'; import { defer as createDefer, Defer } from '../../../kibana_utils/public'; import { buildContextMenuForActions, openContextMenu } from '../context_menu'; import { Trigger } from '../triggers'; interface ExecuteActionTask { action: Action; - context: BaseContext; + context: object; trigger: Trigger; defer: Defer; alwaysShowPopup?: boolean; @@ -44,8 +43,8 @@ export class UiActionsExecutionService { context, trigger, }: { - action: Action; - context: BaseContext; + action: Action; + context: object; trigger: Trigger; }, alwaysShowPopup?: boolean diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts index 39502c3dd17fc..137cfaeac4b67 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts @@ -20,19 +20,17 @@ import { UiActionsService } from './ui_actions_service'; import { Action, ActionInternal, createAction } from '../actions'; import { createHelloWorldAction } from '../tests/test_samples'; -import { TriggerRegistry, TriggerId, ActionType, ActionRegistry } from '../types'; +import { TriggerRegistry, ActionRegistry } from '../types'; import { Trigger } from '../triggers'; -// Casting to ActionType or TriggerId is a hack - in a real situation use -// declare module and add this id to the appropriate context mapping. -const FOO_TRIGGER: TriggerId = 'FOO_TRIGGER' as TriggerId; -const BAR_TRIGGER: TriggerId = 'BAR_TRIGGER' as TriggerId; -const MY_TRIGGER: TriggerId = 'MY_TRIGGER' as TriggerId; +const FOO_TRIGGER = 'FOO_TRIGGER'; +const BAR_TRIGGER = 'BAR_TRIGGER'; +const MY_TRIGGER = 'MY_TRIGGER'; const testAction1: Action = { id: 'action1', order: 1, - type: 'type1' as ActionType, + type: 'type1', execute: async () => {}, getDisplayName: () => 'test1', getIconType: () => '', @@ -42,7 +40,7 @@ const testAction1: Action = { const testAction2: Action = { id: 'action2', order: 2, - type: 'type2' as ActionType, + type: 'type2', execute: async () => {}, getDisplayName: () => 'test2', getIconType: () => '', @@ -99,7 +97,7 @@ describe('UiActionsService', () => { getDisplayName: () => 'test', getIconType: () => '', isCompatible: async () => true, - type: 'test' as ActionType, + type: 'test', }); }); @@ -111,7 +109,7 @@ describe('UiActionsService', () => { getDisplayName: () => 'test', getIconType: () => '', isCompatible: async () => true, - type: 'test' as ActionType, + type: 'test', }); expect(action).toBeInstanceOf(ActionInternal); @@ -123,7 +121,7 @@ describe('UiActionsService', () => { const action1: Action = { id: 'action1', order: 1, - type: 'type1' as ActionType, + type: 'type1', execute: async () => {}, getDisplayName: () => 'test', getIconType: () => '', @@ -132,7 +130,7 @@ describe('UiActionsService', () => { const action2: Action = { id: 'action2', order: 2, - type: 'type2' as ActionType, + type: 'type2', execute: async () => {}, getDisplayName: () => 'test', getIconType: () => '', @@ -207,7 +205,8 @@ describe('UiActionsService', () => { test('filters out actions not applicable based on the context', async () => { const service = new UiActionsService(); const action = createAction({ - type: 'test' as ActionType, + id: 'test', + type: 'test', isCompatible: ({ accept }: { accept: boolean }) => Promise.resolve(accept), execute: () => Promise.resolve(), }); @@ -238,16 +237,15 @@ describe('UiActionsService', () => { test(`throws an error with an invalid trigger ID`, async () => { const service = new UiActionsService(); - // Without the cast "as TriggerId" typescript will happily throw an error! - await expect( - service.getTriggerCompatibleActions('I do not exist' as TriggerId, {}) - ).rejects.toMatchObject(new Error('Trigger [triggerId = I do not exist] does not exist.')); + await expect(service.getTriggerCompatibleActions('I do not exist', {})).rejects.toMatchObject( + new Error('Trigger [triggerId = I do not exist] does not exist.') + ); }); test('returns empty list if trigger not attached to any action', async () => { const service = new UiActionsService(); const testTrigger: Trigger = { - id: '123' as TriggerId, + id: '123', title: '123', }; service.registerTrigger(testTrigger); @@ -445,9 +443,7 @@ describe('UiActionsService', () => { } as any; service.registerAction(action); - expect(() => - service.detachAction('i do not exist' as TriggerId, ACTION_HELLO_WORLD) - ).toThrowError( + expect(() => service.detachAction('i do not exist', ACTION_HELLO_WORLD)).toThrowError( 'No trigger [triggerId = i do not exist] exists, for detaching action [actionId = ACTION_HELLO_WORLD].' ); }); @@ -461,7 +457,7 @@ describe('UiActionsService', () => { } as any; service.registerAction(action); - expect(() => service.addTriggerAction('i do not exist' as TriggerId, action)).toThrowError( + expect(() => service.addTriggerAction('i do not exist', action)).toThrowError( 'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = ACTION_HELLO_WORLD].' ); }); diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index ec5f3afa19c94..456341d98c5b5 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -17,15 +17,9 @@ * under the License. */ -import { - TriggerRegistry, - ActionRegistry, - TriggerToActionsRegistry, - TriggerId, - TriggerContextMapping, -} from '../types'; +import { TriggerRegistry, ActionRegistry, TriggerToActionsRegistry } from '../types'; import { ActionInternal, Action, ActionDefinition, ActionContext } from '../actions'; -import { Trigger, TriggerContext } from '../triggers/trigger'; +import { Trigger } from '../triggers/trigger'; import { TriggerInternal } from '../triggers/trigger_internal'; import { TriggerContract } from '../triggers/trigger_contract'; import { UiActionsExecutionService } from './ui_actions_execution_service'; @@ -67,7 +61,7 @@ export class UiActionsService { this.triggerToActions.set(trigger.id, []); }; - public readonly getTrigger = (triggerId: T): TriggerContract => { + public readonly getTrigger = (triggerId: string): TriggerContract => { const trigger = this.triggers.get(triggerId); if (!trigger) { @@ -103,7 +97,7 @@ export class UiActionsService { return this.actions.has(actionId); }; - public readonly attachAction = (triggerId: T, actionId: string): void => { + public readonly attachAction = (triggerId: string, actionId: string): void => { const trigger = this.triggers.get(triggerId); if (!trigger) { @@ -119,7 +113,7 @@ export class UiActionsService { } }; - public readonly detachAction = (triggerId: TriggerId, actionId: string) => { + public readonly detachAction = (triggerId: string, actionId: string) => { const trigger = this.triggers.get(triggerId); if (!trigger) { @@ -139,14 +133,10 @@ export class UiActionsService { /** * `addTriggerAction` is similar to `attachAction` as it attaches action to a * trigger, but it also registers the action, if it has not been registered, yet. - * - * `addTriggerAction` also infers better typing of the `action` argument. */ - public readonly addTriggerAction = ( - triggerId: T, - // The action can accept partial or no context, but if it needs context not provided - // by this type of trigger, typescript will complain. yay! - action: ActionDefinition | Action // TODO: remove `Action` https://github.com/elastic/kibana/issues/74501 + public readonly addTriggerAction = ( + triggerId: string, + action: ActionDefinition // TODO: remove `Action` https://github.com/elastic/kibana/issues/74501 ): void => { if (!this.actions.has(action.id)) this.registerAction(action); this.attachAction(triggerId, action.id); @@ -162,9 +152,7 @@ export class UiActionsService { return this.actions.get(id) as ActionInternal; }; - public readonly getTriggerActions = ( - triggerId: T - ): Array> => { + public readonly getTriggerActions = (triggerId: string): Action[] => { // This line checks if trigger exists, otherwise throws. this.getTrigger!(triggerId); @@ -174,13 +162,13 @@ export class UiActionsService { .map((actionId) => this.actions.get(actionId) as ActionInternal) .filter(Boolean); - return actions as Array>>; + return actions as Action[]; }; - public readonly getTriggerCompatibleActions = async ( - triggerId: T, - context: TriggerContextMapping[T] - ): Promise>> => { + public readonly getTriggerCompatibleActions = async ( + triggerId: string, + context: object + ): Promise => { const actions = this.getTriggerActions!(triggerId); const isCompatibles = await Promise.all( actions.map((action) => @@ -191,8 +179,7 @@ export class UiActionsService { ) ); return actions.reduce( - (acc: Array>, action, i) => - isCompatibles[i] ? [...acc, action] : acc, + (acc: Action[], action, i) => (isCompatibles[i] ? [...acc, action] : acc), [] ); }; @@ -202,11 +189,8 @@ export class UiActionsService { * * Use `plugins.uiActions.getTrigger(triggerId).exec(params)` instead. */ - public readonly executeTriggerActions = async ( - triggerId: T, - context: TriggerContext - ) => { - const trigger = this.getTrigger(triggerId); + public readonly executeTriggerActions = async (triggerId: string, context: object) => { + const trigger = this.getTrigger(triggerId); await trigger.exec(context); }; diff --git a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts index 51ba165ba730b..6f8a45cc2e40a 100644 --- a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts @@ -21,7 +21,6 @@ import { Action, createAction } from '../actions'; import { openContextMenu } from '../context_menu'; import { uiActionsPluginMock } from '../mocks'; import { Trigger } from '../triggers'; -import { TriggerId, ActionType } from '../types'; import { waitFor } from '@testing-library/dom'; jest.mock('../context_menu'); @@ -31,17 +30,13 @@ const openContextMenuSpy = (openContextMenu as any) as jest.SpyInstance; const CONTACT_USER_TRIGGER = 'CONTACT_USER_TRIGGER'; -// Casting to ActionType is a hack - in a real situation use -// declare module and add this id to ActionContextMapping. -const TEST_ACTION_TYPE = 'TEST_ACTION_TYPE' as ActionType; - function createTestAction( type: string, checkCompatibility: (context: C) => boolean, autoExecutable = false ): Action { - return createAction({ - type: type as ActionType, + return createAction({ + type, id: type, isCompatible: (context: C) => Promise.resolve(checkCompatibility(context)), execute: (context) => executeFn(context), @@ -67,7 +62,7 @@ beforeEach(reset); test('executes a single action mapped to a trigger', async () => { const { setup, doStart } = uiActions; const trigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; const action = createTestAction('test1', () => true); @@ -77,7 +72,7 @@ test('executes a single action mapped to a trigger', async () => { const context = {}; const start = doStart(); - await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + await start.executeTriggerActions('MY-TRIGGER', context); jest.runAllTimers(); @@ -88,7 +83,7 @@ test('executes a single action mapped to a trigger', async () => { test("doesn't throw an error if there are no compatible actions to execute", async () => { const { setup, doStart } = uiActions; const trigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; @@ -96,15 +91,13 @@ test("doesn't throw an error if there are no compatible actions to execute", asy const context = {}; const start = doStart(); - await expect( - start.executeTriggerActions('MY-TRIGGER' as TriggerId, context) - ).resolves.toBeUndefined(); + await expect(start.executeTriggerActions('MY-TRIGGER', context)).resolves.toBeUndefined(); }); test('does not execute an incompatible action', async () => { const { setup, doStart } = uiActions; const trigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; const action = createTestAction<{ name: string }>( @@ -119,7 +112,7 @@ test('does not execute an incompatible action', async () => { const context = { name: 'executeme', }; - await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + await start.executeTriggerActions('MY-TRIGGER', context); jest.runAllTimers(); @@ -129,7 +122,7 @@ test('does not execute an incompatible action', async () => { test('shows a context menu when more than one action is mapped to a trigger', async () => { const { setup, doStart } = uiActions; const trigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; const action1 = createTestAction('test1', () => true); @@ -143,7 +136,7 @@ test('shows a context menu when more than one action is mapped to a trigger', as const start = doStart(); const context = {}; - await start.getTrigger('MY-TRIGGER' as TriggerId)!.exec(context); + await start.getTrigger('MY-TRIGGER')!.exec(context); jest.runAllTimers(); @@ -156,7 +149,7 @@ test('shows a context menu when more than one action is mapped to a trigger', as test('shows a context menu when there is only one action mapped to a trigger and "alwaysShowPopup" is set', async () => { const { setup, doStart } = uiActions; const trigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; const action1 = createTestAction('test1', () => true); @@ -168,7 +161,7 @@ test('shows a context menu when there is only one action mapped to a trigger and const start = doStart(); const context = {}; - await start.getTrigger('MY-TRIGGER' as TriggerId)!.exec(context, true); + await start.getTrigger('MY-TRIGGER')!.exec(context, true); jest.runAllTimers(); @@ -181,7 +174,7 @@ test('shows a context menu when there is only one action mapped to a trigger and test('passes whole action context to isCompatible()', async () => { const { setup, doStart } = uiActions; const trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; const action = createTestAction<{ foo: string }>('test', ({ foo }) => { @@ -195,14 +188,14 @@ test('passes whole action context to isCompatible()', async () => { const start = doStart(); const context = { foo: 'bar' }; - await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + await start.executeTriggerActions('MY-TRIGGER', context); jest.runAllTimers(); }); test("doesn't show a context menu for auto executable actions", async () => { const { setup, doStart } = uiActions; const trigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; const action1 = createTestAction('test1', () => true, true); @@ -216,7 +209,7 @@ test("doesn't show a context menu for auto executable actions", async () => { const start = doStart(); const context = {}; - await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + await start.executeTriggerActions('MY-TRIGGER', context); jest.runAllTimers(); @@ -229,7 +222,7 @@ test("doesn't show a context menu for auto executable actions", async () => { test('passes trigger into execute', async () => { const { setup, doStart } = uiActions; const trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; const action = createTestAction<{ foo: string }>('test', () => true); @@ -240,7 +233,7 @@ test('passes trigger into execute', async () => { const start = doStart(); const context = { foo: 'bar' }; - await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + await start.executeTriggerActions('MY-TRIGGER', context); jest.runAllTimers(); expect(executeFn).toBeCalledWith({ ...context, diff --git a/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts index 55ccac42ff255..ddae251bb83d2 100644 --- a/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts @@ -19,17 +19,16 @@ import { ActionInternal, Action } from '../actions'; import { uiActionsPluginMock } from '../mocks'; -import { TriggerId, ActionType } from '../types'; const action1: Action = { id: 'action1', order: 1, - type: 'type1' as ActionType, + type: 'type1', } as any; const action2: Action = { id: 'action2', order: 2, - type: 'type2' as ActionType, + type: 'type2', } as any; test('returns actions set on trigger', () => { @@ -38,24 +37,24 @@ test('returns actions set on trigger', () => { setup.registerAction(action2); setup.registerTrigger({ description: 'foo', - id: 'trigger' as TriggerId, + id: 'trigger', title: 'baz', }); const start = doStart(); - const list0 = start.getTriggerActions('trigger' as TriggerId); + const list0 = start.getTriggerActions('trigger'); expect(list0).toHaveLength(0); - setup.addTriggerAction('trigger' as TriggerId, action1); - const list1 = start.getTriggerActions('trigger' as TriggerId); + setup.addTriggerAction('trigger', action1); + const list1 = start.getTriggerActions('trigger'); expect(list1).toHaveLength(1); expect(list1[0]).toBeInstanceOf(ActionInternal); expect(list1[0].id).toBe(action1.id); - setup.addTriggerAction('trigger' as TriggerId, action2); - const list2 = start.getTriggerActions('trigger' as TriggerId); + setup.addTriggerAction('trigger', action2); + const list2 = start.getTriggerActions('trigger'); expect(list2).toHaveLength(2); expect(!!list2.find(({ id }: any) => id === 'action1')).toBe(true); diff --git a/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts b/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts index 21dd17ed82e3f..400a9453fe267 100644 --- a/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts @@ -21,23 +21,23 @@ import { uiActionsPluginMock } from '../mocks'; import { createHelloWorldAction } from '../tests/test_samples'; import { Action, createAction } from '../actions'; import { Trigger } from '../triggers'; -import { TriggerId, ActionType } from '../types'; -let action: Action<{ name: string }, ActionType>; +let action: Action<{ name: string }>; let uiActions: ReturnType; beforeEach(() => { uiActions = uiActionsPluginMock.createPlugin(); action = createAction({ - type: 'test' as ActionType, + id: 'test', + type: 'test', execute: () => Promise.resolve(), }); uiActions.setup.registerAction(action); uiActions.setup.registerTrigger({ - id: 'trigger' as TriggerId, + id: 'trigger', title: 'trigger', }); - uiActions.setup.addTriggerAction('trigger' as TriggerId, action); + uiActions.setup.addTriggerAction('trigger', action); }); test('can register action', async () => { @@ -54,14 +54,14 @@ test('getTriggerCompatibleActions returns attached actions', async () => { setup.registerAction(helloWorldAction); const testTrigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER', title: 'My trigger', }; setup.registerTrigger(testTrigger); - setup.addTriggerAction('MY-TRIGGER' as TriggerId, helloWorldAction); + setup.addTriggerAction('MY-TRIGGER', helloWorldAction); const start = doStart(); - const actions = await start.getTriggerCompatibleActions('MY-TRIGGER' as TriggerId, {}); + const actions = await start.getTriggerCompatibleActions('MY-TRIGGER', {}); expect(actions.length).toBe(1); expect(actions[0].id).toBe(helloWorldAction.id); @@ -70,7 +70,8 @@ test('getTriggerCompatibleActions returns attached actions', async () => { test('filters out actions not applicable based on the context', async () => { const { setup, doStart } = uiActions; const action1 = createAction({ - type: 'test1' as ActionType, + id: 'test1', + type: 'test1', isCompatible: async (context: { accept: boolean }) => { return Promise.resolve(context.accept); }, @@ -78,7 +79,7 @@ test('filters out actions not applicable based on the context', async () => { }); const testTrigger: Trigger = { - id: 'MY-TRIGGER2' as TriggerId, + id: 'MY-TRIGGER2', title: 'My trigger', }; @@ -100,15 +101,15 @@ test(`throws an error with an invalid trigger ID`, async () => { const { doStart } = uiActions; const start = doStart(); - await expect( - start.getTriggerCompatibleActions('I do not exist' as TriggerId, {}) - ).rejects.toMatchObject(new Error('Trigger [triggerId = I do not exist] does not exist.')); + await expect(start.getTriggerCompatibleActions('I do not exist', {})).rejects.toMatchObject( + new Error('Trigger [triggerId = I do not exist] does not exist.') + ); }); test(`with a trigger mapping that maps to an non-existing action returns empty list`, async () => { const { setup, doStart } = uiActions; const testTrigger: Trigger = { - id: '123' as TriggerId, + id: '123', title: '123', }; setup.registerTrigger(testTrigger); diff --git a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx index a4cfe172dd109..f9e1c0261e1a1 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx +++ b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx @@ -20,9 +20,8 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiFlyoutBody } from '@elastic/eui'; import { CoreStart } from 'src/core/public'; -import { createAction, ActionByType } from '../../actions'; +import { createAction, Action } from '../../actions'; import { toMountPoint, reactToUiComponent } from '../../../../kibana_react/public'; -import { ActionType } from '../../types'; const ReactMenuItem: React.FC = () => { return ( @@ -37,14 +36,11 @@ const ReactMenuItem: React.FC = () => { const UiMenuItem = reactToUiComponent(ReactMenuItem); -// Casting to ActionType is a hack - in a real situation use -// declare module and add this id to ActionContextMapping. -export const ACTION_HELLO_WORLD = 'ACTION_HELLO_WORLD' as ActionType; +export const ACTION_HELLO_WORLD = 'ACTION_HELLO_WORLD'; -export function createHelloWorldAction( - overlays: CoreStart['overlays'] -): ActionByType { - return createAction({ +export function createHelloWorldAction(overlays: CoreStart['overlays']): Action { + return createAction({ + id: ACTION_HELLO_WORLD, type: ACTION_HELLO_WORLD, getIconType: () => 'lock', MenuItem: UiMenuItem, diff --git a/src/plugins/ui_actions/public/triggers/default_trigger.ts b/src/plugins/ui_actions/public/triggers/default_trigger.ts index 74be0243bdac5..107cb3bda5b7d 100644 --- a/src/plugins/ui_actions/public/triggers/default_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/default_trigger.ts @@ -20,7 +20,7 @@ import { Trigger } from '.'; export const DEFAULT_TRIGGER = ''; -export const defaultTrigger: Trigger<''> = { +export const defaultTrigger: Trigger = { id: DEFAULT_TRIGGER, title: 'Unknown', description: 'Unknown trigger.', diff --git a/src/plugins/ui_actions/public/triggers/row_click_trigger.ts b/src/plugins/ui_actions/public/triggers/row_click_trigger.ts index 0fc261b3e1fb3..77d0090406409 100644 --- a/src/plugins/ui_actions/public/triggers/row_click_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/row_click_trigger.ts @@ -19,11 +19,11 @@ import { i18n } from '@kbn/i18n'; import { Trigger } from '.'; -import { Datatable } from '../../../expressions'; +import type { Datatable } from '../../../expressions'; export const ROW_CLICK_TRIGGER = 'ROW_CLICK_TRIGGER'; -export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'> = { +export const rowClickTrigger: Trigger = { id: ROW_CLICK_TRIGGER, title: i18n.translate('uiActions.triggers.rowClickTitle', { defaultMessage: 'Table row click', diff --git a/src/plugins/ui_actions/public/triggers/trigger.ts b/src/plugins/ui_actions/public/triggers/trigger.ts index 1b1231c132dde..b4def56d21395 100644 --- a/src/plugins/ui_actions/public/triggers/trigger.ts +++ b/src/plugins/ui_actions/public/triggers/trigger.ts @@ -17,8 +17,6 @@ * under the License. */ -import { TriggerContextMapping, TriggerId } from '../types'; - /** * This is a convenience interface used to register a *trigger*. * @@ -30,11 +28,11 @@ import { TriggerContextMapping, TriggerId } from '../types'; * trigger is *called* it first displays a context menu for user to pick a * single action to execute. */ -export interface Trigger { +export interface Trigger { /** * Unique name of the trigger as identified in `ui_actions` plugin trigger registry. */ - id: ID; + id: string; /** * User friendly name of the trigger. @@ -46,5 +44,3 @@ export interface Trigger { */ description?: string; } - -export type TriggerContext = T extends TriggerId ? TriggerContextMapping[T] : never; diff --git a/src/plugins/ui_actions/public/triggers/trigger_contract.ts b/src/plugins/ui_actions/public/triggers/trigger_contract.ts index 7e7fba0ba80d3..95e856f4d3eaa 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_contract.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_contract.ts @@ -18,16 +18,15 @@ */ import { TriggerInternal } from './trigger_internal'; -import { TriggerId, TriggerContextMapping } from '../types'; /** * This is a public representation of a trigger that is provided to other plugins. */ -export class TriggerContract { +export class TriggerContract { /** * Unique name of the trigger as identified in `ui_actions` plugin trigger registry. */ - public readonly id: T; + public readonly id: string; /** * User friendly name of the trigger. @@ -39,7 +38,7 @@ export class TriggerContract { */ public readonly description?: string; - constructor(private readonly internal: TriggerInternal) { + constructor(private readonly internal: TriggerInternal) { this.id = this.internal.trigger.id; this.title = this.internal.trigger.title; this.description = this.internal.trigger.description; @@ -48,7 +47,7 @@ export class TriggerContract { /** * Use this method to execute action attached to this trigger. */ - public readonly exec = async (context: TriggerContextMapping[T], alwaysShowPopup?: boolean) => { + public readonly exec = async (context: Context, alwaysShowPopup?: boolean) => { await this.internal.execute(context, alwaysShowPopup); }; } diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index fd43a020504c0..e78b2beaabc8e 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -20,18 +20,17 @@ import { Trigger } from './trigger'; import { TriggerContract } from './trigger_contract'; import { UiActionsService } from '../service'; -import { TriggerId, TriggerContextMapping } from '../types'; /** * Internal representation of a trigger kept for consumption only internally * within `ui_actions` plugin. */ -export class TriggerInternal { - public readonly contract = new TriggerContract(this); +export class TriggerInternal { + public readonly contract: TriggerContract = new TriggerContract(this); - constructor(public readonly service: UiActionsService, public readonly trigger: Trigger) {} + constructor(public readonly service: UiActionsService, public readonly trigger: Trigger) {} - public async execute(context: TriggerContextMapping[T], alwaysShowPopup?: boolean) { + public async execute(context: Context, alwaysShowPopup?: boolean) { const triggerId = this.trigger.id; const actions = await this.service.getTriggerCompatibleActions!(triggerId, context); diff --git a/src/plugins/ui_actions/public/triggers/visualize_field_trigger.ts b/src/plugins/ui_actions/public/triggers/visualize_field_trigger.ts index 4f3c5f613eddf..0c85f4d38dee4 100644 --- a/src/plugins/ui_actions/public/triggers/visualize_field_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/visualize_field_trigger.ts @@ -20,7 +20,7 @@ import { Trigger } from '.'; export const VISUALIZE_FIELD_TRIGGER = 'VISUALIZE_FIELD_TRIGGER'; -export const visualizeFieldTrigger: Trigger<'VISUALIZE_FIELD_TRIGGER'> = { +export const visualizeFieldTrigger: Trigger = { id: VISUALIZE_FIELD_TRIGGER, title: 'Visualize field', description: 'Triggered when user wants to visualize a field.', diff --git a/src/plugins/ui_actions/public/triggers/visualize_geo_field_trigger.ts b/src/plugins/ui_actions/public/triggers/visualize_geo_field_trigger.ts index 5582b3b42660c..0ee0be3a644d4 100644 --- a/src/plugins/ui_actions/public/triggers/visualize_geo_field_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/visualize_geo_field_trigger.ts @@ -20,7 +20,7 @@ import { Trigger } from '.'; export const VISUALIZE_GEO_FIELD_TRIGGER = 'VISUALIZE_GEO_FIELD_TRIGGER'; -export const visualizeGeoFieldTrigger: Trigger<'VISUALIZE_GEO_FIELD_TRIGGER'> = { +export const visualizeGeoFieldTrigger: Trigger = { id: VISUALIZE_GEO_FIELD_TRIGGER, title: 'Visualize Geo field', description: 'Triggered when user wants to visualize a geo field.', diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 62fac245514cd..dc3b25f69f513 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -19,17 +19,10 @@ import { ActionInternal } from './actions/action_internal'; import { TriggerInternal } from './triggers/trigger_internal'; -import { - ROW_CLICK_TRIGGER, - VISUALIZE_FIELD_TRIGGER, - VISUALIZE_GEO_FIELD_TRIGGER, - DEFAULT_TRIGGER, - RowClickContext, -} from './triggers'; -export type TriggerRegistry = Map>; +export type TriggerRegistry = Map>; export type ActionRegistry = Map; -export type TriggerToActionsRegistry = Map; +export type TriggerToActionsRegistry = Map; export interface VisualizeFieldContext { fieldName: string; @@ -37,27 +30,6 @@ export interface VisualizeFieldContext { contextualFields?: string[]; } -export type TriggerId = keyof TriggerContextMapping; - -export type BaseContext = object; -export type TriggerContext = BaseContext; - -export interface TriggerContextMapping { - [DEFAULT_TRIGGER]: TriggerContext; - [ROW_CLICK_TRIGGER]: RowClickContext; - [VISUALIZE_FIELD_TRIGGER]: VisualizeFieldContext; - [VISUALIZE_GEO_FIELD_TRIGGER]: VisualizeFieldContext; -} - -const DEFAULT_ACTION = ''; export const ACTION_VISUALIZE_FIELD = 'ACTION_VISUALIZE_FIELD'; export const ACTION_VISUALIZE_GEO_FIELD = 'ACTION_VISUALIZE_GEO_FIELD'; export const ACTION_VISUALIZE_LENS_FIELD = 'ACTION_VISUALIZE_LENS_FIELD'; -export type ActionType = keyof ActionContextMapping; - -export interface ActionContextMapping { - [DEFAULT_ACTION]: BaseContext; - [ACTION_VISUALIZE_FIELD]: VisualizeFieldContext; - [ACTION_VISUALIZE_GEO_FIELD]: VisualizeFieldContext; - [ACTION_VISUALIZE_LENS_FIELD]: VisualizeFieldContext; -} diff --git a/src/plugins/ui_actions/public/util/presentable.ts b/src/plugins/ui_actions/public/util/presentable.ts index 59440d6c75976..c586a996920ac 100644 --- a/src/plugins/ui_actions/public/util/presentable.ts +++ b/src/plugins/ui_actions/public/util/presentable.ts @@ -22,7 +22,7 @@ import { UiComponent } from 'src/plugins/kibana_utils/public'; /** * Represents something that can be displayed to user in UI. */ -export interface Presentable { +export interface Presentable { /** * ID that uniquely identifies this object. */ @@ -77,11 +77,11 @@ export interface Presentable { readonly grouping?: PresentableGrouping; } -export interface PresentableGroup +export interface PresentableGroup extends Partial< Pick, 'getDisplayName' | 'getDisplayNameTooltip' | 'getIconType' | 'order'> > { id: string; } -export type PresentableGrouping = Array>; +export type PresentableGrouping = Array>; diff --git a/src/plugins/ui_actions/tsconfig.json b/src/plugins/ui_actions/tsconfig.json new file mode 100644 index 0000000000000..a871d7215cdc5 --- /dev/null +++ b/src/plugins/ui_actions/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + ] +} diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 85c910cd09bf1..9e47557e71a98 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -95,10 +95,10 @@ Some background: - `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. -- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `callCluster`, `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. +- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. In some scenarios, your collector might need to maintain its own client. An example of that is the `monitoring` plugin, that maintains a connection to the Remote Monitoring Cluster to push its monitoring data. If that's the case, your plugin can opt-in to receive the additional `kibanaRequest` parameter by adding `extendFetchContext.kibanaRequest: true` to the collector's config: it will be appended to the context of the `fetch` method only if the request needs to be scoped to a user other than Kibana Internal, so beware that your collector will need to work for both scenarios (especially for the scenario when `kibanaRequest` is missing). -Note: there will be many cases where you won't need to use the `callCluster`, `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. +Note: there will be many cases where you won't need to use the `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. In the case of using a custom SavedObjects client, it is up to the plugin to initialize the client to save the data and it is strongly recommended to scope that client to the `kibana_system` user. @@ -231,6 +231,7 @@ export class DashboardPlugin implements Plugin { ); } } +``` ## Schema Field @@ -302,7 +303,7 @@ New fields added to the telemetry payload currently mean that telemetry cluster There are a few ways you can test that your usage collector is working properly. -1. The `/api/stats?extended=true&legacy=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. +1. The `/api/stats?extended=true&legacy=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where the elasticsearch client wraps the call with the request. 2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. @@ -390,7 +391,7 @@ To track multiple metrics within a single request, provide an array of events usageCollection.reportUiCounter(``, METRIC_TYPE.CLICK, [``, ``]); ``` -### Increamenting counter by more than 1 +### Incrementing counter by more than 1 To track an event occurance more than once in the same call, provide a 4th argument to the `reportUiCounter` function: diff --git a/src/plugins/usage_collection/public/components/track_application_view/index.ts b/src/plugins/usage_collection/public/components/track_application_view/index.ts new file mode 100644 index 0000000000000..18369fb4acfad --- /dev/null +++ b/src/plugins/usage_collection/public/components/track_application_view/index.ts @@ -0,0 +1,21 @@ +/* + * 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 type { TrackApplicationViewProps } from './types'; +export { ApplicationUsageContext, TrackApplicationView } from './track_application_view'; diff --git a/src/plugins/usage_collection/public/components/track_application_view/track_application_view.test.tsx b/src/plugins/usage_collection/public/components/track_application_view/track_application_view.test.tsx new file mode 100644 index 0000000000000..118ea4defbd06 --- /dev/null +++ b/src/plugins/usage_collection/public/components/track_application_view/track_application_view.test.tsx @@ -0,0 +1,59 @@ +/* + * 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 { mountWithIntl } from '@kbn/test/jest'; +import { ApplicationUsageContext, TrackApplicationView } from './track_application_view'; +import { IApplicationUsageTracker } from '../../plugin'; +import { fireEvent } from '@testing-library/react'; + +describe('TrackApplicationView', () => { + test('it renders the internal component even when no tracker has been set', () => { + const component = mountWithIntl( + +

Hello

+
+ ); + component.unmount(); + }); + + test('it tracks the component while it is rendered', () => { + const applicationUsageTrackerMock: jest.Mocked = { + trackApplicationViewUsage: jest.fn(), + flushTrackedView: jest.fn(), + updateViewClickCounter: jest.fn(), + }; + expect(applicationUsageTrackerMock.trackApplicationViewUsage).not.toHaveBeenCalled(); + const viewId = 'testView'; + const component = mountWithIntl( + + +

Hello

+
+
+ ); + expect(applicationUsageTrackerMock.trackApplicationViewUsage).toHaveBeenCalledWith(viewId); + expect(applicationUsageTrackerMock.updateViewClickCounter).not.toHaveBeenCalled(); + fireEvent.click(component.getDOMNode()); + expect(applicationUsageTrackerMock.updateViewClickCounter).toHaveBeenCalledWith(viewId); + expect(applicationUsageTrackerMock.flushTrackedView).not.toHaveBeenCalled(); + component.unmount(); + expect(applicationUsageTrackerMock.flushTrackedView).toHaveBeenCalledWith(viewId); + }); +}); diff --git a/src/plugins/usage_collection/public/components/track_application_view/track_application_view.tsx b/src/plugins/usage_collection/public/components/track_application_view/track_application_view.tsx new file mode 100644 index 0000000000000..e7d7c38bc9460 --- /dev/null +++ b/src/plugins/usage_collection/public/components/track_application_view/track_application_view.tsx @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { createContext, FC } from 'react'; +import { TrackApplicationViewComponent } from './track_application_view_component'; +import { IApplicationUsageTracker } from '../../plugin'; +import { TrackApplicationViewProps } from './types'; + +export const ApplicationUsageContext = createContext( + undefined +); + +/** + * React component to track the number of clicks and minutes on screen of the children components. + * @param props {@Link TrackApplicationViewProps} + * @constructor + */ +export const TrackApplicationView: FC = (props) => { + return ( + + {(value) => { + const propsWithTracker = { ...props, applicationUsageTracker: value }; + return ; + }} + + ); +}; diff --git a/src/plugins/usage_collection/public/components/track_application_view/track_application_view_component.test.tsx b/src/plugins/usage_collection/public/components/track_application_view/track_application_view_component.test.tsx new file mode 100644 index 0000000000000..c184a29a52d3e --- /dev/null +++ b/src/plugins/usage_collection/public/components/track_application_view/track_application_view_component.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 { mountWithIntl } from '@kbn/test/jest'; +import { TrackApplicationViewComponent } from './track_application_view_component'; +import { IApplicationUsageTracker } from '../../plugin'; +import { fireEvent } from '@testing-library/react'; + +describe('TrackApplicationViewComponent', () => { + test('it renders the internal component even when no tracker is provided', () => { + const component = mountWithIntl( + +

Hello

+
+ ); + component.unmount(); + }); + + test('it tracks the component while it is rendered', () => { + const applicationUsageTrackerMock: jest.Mocked = { + trackApplicationViewUsage: jest.fn(), + flushTrackedView: jest.fn(), + updateViewClickCounter: jest.fn(), + }; + expect(applicationUsageTrackerMock.trackApplicationViewUsage).not.toHaveBeenCalled(); + const viewId = 'testView'; + const component = mountWithIntl( + +

Hello

+
+ ); + expect(applicationUsageTrackerMock.trackApplicationViewUsage).toHaveBeenCalledWith(viewId); + expect(applicationUsageTrackerMock.updateViewClickCounter).not.toHaveBeenCalled(); + fireEvent.click(component.getDOMNode()); + expect(applicationUsageTrackerMock.updateViewClickCounter).toHaveBeenCalledWith(viewId); + expect(applicationUsageTrackerMock.flushTrackedView).not.toHaveBeenCalled(); + component.unmount(); + expect(applicationUsageTrackerMock.flushTrackedView).toHaveBeenCalledWith(viewId); + }); +}); diff --git a/src/plugins/usage_collection/public/components/track_application_view.tsx b/src/plugins/usage_collection/public/components/track_application_view/track_application_view_component.tsx similarity index 83% rename from src/plugins/usage_collection/public/components/track_application_view.tsx rename to src/plugins/usage_collection/public/components/track_application_view/track_application_view_component.tsx index 00011dd0a0eb1..8c8acbf4fc9fb 100644 --- a/src/plugins/usage_collection/public/components/track_application_view.tsx +++ b/src/plugins/usage_collection/public/components/track_application_view/track_application_view_component.tsx @@ -17,17 +17,16 @@ * under the License. */ -import { Component, ReactNode } from 'react'; +import { Component } from 'react'; import ReactDOM from 'react-dom'; -import { UsageCollectionSetup } from '../plugin'; +import { IApplicationUsageTracker } from '../../plugin'; +import { TrackApplicationViewProps } from './types'; -interface Props { - viewId: string; - applicationUsageTracker?: UsageCollectionSetup['applicationUsageTracker']; - children: ReactNode; +interface Props extends TrackApplicationViewProps { + applicationUsageTracker?: IApplicationUsageTracker; } -export class TrackApplicationView extends Component { +export class TrackApplicationViewComponent extends Component { onClick = () => { const { applicationUsageTracker, viewId } = this.props; applicationUsageTracker?.updateViewClickCounter(viewId); diff --git a/src/plugins/usage_collection/public/components/track_application_view/types.ts b/src/plugins/usage_collection/public/components/track_application_view/types.ts new file mode 100644 index 0000000000000..ae9928f166c53 --- /dev/null +++ b/src/plugins/usage_collection/public/components/track_application_view/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { ReactNode } from 'react'; + +/** + * Props to provide to the {@Link TrackApplicationView} component. + * @public + */ +export interface TrackApplicationViewProps { + /** + * The name of the view to be tracked. The appId will be obtained automatically. + * @public + */ + viewId: string; + /** + * The React component to be tracked. + * @public + */ + children: ReactNode; +} diff --git a/src/plugins/usage_collection/public/mocks.ts b/src/plugins/usage_collection/public/mocks.tsx similarity index 83% rename from src/plugins/usage_collection/public/mocks.ts rename to src/plugins/usage_collection/public/mocks.tsx index b6fe56cf0a93c..0ae3c74f1b0a1 100644 --- a/src/plugins/usage_collection/public/mocks.ts +++ b/src/plugins/usage_collection/public/mocks.tsx @@ -17,8 +17,10 @@ * under the License. */ +import React from 'react'; import { ApplicationUsageTracker } from '@kbn/analytics'; import { UsageCollectionSetup, METRIC_TYPE } from '.'; +import { ApplicationUsageContext } from './components/track_application_view'; export type Setup = jest.Mocked; @@ -34,6 +36,13 @@ export const createApplicationUsageTrackerMock = (): ApplicationUsageTracker => const createSetupContract = (): Setup => { const applicationUsageTrackerMock = createApplicationUsageTrackerMock(); const setupContract: Setup = { + components: { + ApplicationUsageTrackingProvider: (props) => ( + + {props.children} + + ), + }, applicationUsageTracker: applicationUsageTrackerMock, allowTrackUserAgent: jest.fn(), reportUiCounter: jest.fn(), diff --git a/src/plugins/usage_collection/public/plugin.ts b/src/plugins/usage_collection/public/plugin.tsx similarity index 74% rename from src/plugins/usage_collection/public/plugin.ts rename to src/plugins/usage_collection/public/plugin.tsx index c31de270d3a92..d24e14bf5ef20 100644 --- a/src/plugins/usage_collection/public/plugin.ts +++ b/src/plugins/usage_collection/public/plugin.tsx @@ -19,6 +19,7 @@ import { Reporter, METRIC_TYPE, ApplicationUsageTracker } from '@kbn/analytics'; import { Subject, merge, Subscription } from 'rxjs'; +import React from 'react'; import { Storage } from '../../kibana_utils/public'; import { createReporter, trackApplicationUsageChange } from './services'; import { @@ -28,6 +29,7 @@ import { CoreStart, HttpSetup, } from '../../../core/public'; +import { ApplicationUsageContext } from './components/track_application_view'; export interface PublicConfigType { uiCounters: { @@ -35,13 +37,17 @@ export interface PublicConfigType { debug: boolean; }; } +export type IApplicationUsageTracker = Pick< + ApplicationUsageTracker, + 'trackApplicationViewUsage' | 'flushTrackedView' | 'updateViewClickCounter' +>; export interface UsageCollectionSetup { + components: { + ApplicationUsageTrackingProvider: React.FC; + }; allowTrackUserAgent: (allow: boolean) => void; - applicationUsageTracker: Pick< - ApplicationUsageTracker, - 'trackApplicationViewUsage' | 'flushTrackedView' | 'updateViewClickCounter' - >; + applicationUsageTracker: IApplicationUsageTracker; reportUiCounter: Reporter['reportUiCounter']; METRIC_TYPE: typeof METRIC_TYPE; __LEGACY: { @@ -92,18 +98,17 @@ export class UsageCollectionPlugin implements Plugin ( + + {props.children} + ), }, + applicationUsageTracker, allowTrackUserAgent: (allow: boolean) => { this.trackUserAgent = allow; }, @@ -134,17 +139,7 @@ export class UsageCollectionPlugin implements Plugin subscription.unsubscribe()); } } + + private getPublicApplicationUsageTracker(): IApplicationUsageTracker { + // Using this.applicationUsageTracker! because this private method is only called once it's initialised + return { + trackApplicationViewUsage: this.applicationUsageTracker!.trackApplicationViewUsage.bind( + this.applicationUsageTracker + ), + flushTrackedView: this.applicationUsageTracker!.flushTrackedView.bind( + this.applicationUsageTracker + ), + updateViewClickCounter: this.applicationUsageTracker!.updateViewClickCounter.bind( + this.applicationUsageTracker + ), + }; + } } diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 8e86bc3d1cd26..afd5b5883ff17 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -19,7 +19,6 @@ import { Logger, - LegacyAPICaller, ElasticsearchClient, ISavedObjectsRepository, SavedObjectsClientContract, @@ -54,10 +53,6 @@ export type MakeSchemaFrom = { * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. */ export type CollectorFetchContext = { - /** - * @deprecated Scoped Legacy Elasticsearch client: use esClient instead - */ - callCluster: LegacyAPICaller; /** * Request-scoped Elasticsearch client * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 90a69043e0635..310714cc2a48a 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -44,7 +44,6 @@ describe('CollectorSet', () => { loggerSpies.debug.mockRestore(); loggerSpies.warn.mockRestore(); }); - const mockCallCluster = jest.fn().mockResolvedValue({ passTest: 1000 }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const mockSoClient = savedObjectsRepositoryMock.create(); const req = void 0; // No need to instantiate any KibanaRequest in these tests @@ -83,18 +82,19 @@ describe('CollectorSet', () => { }); it('should log debug status of fetching from the collector', async () => { + mockEsClient.get.mockResolvedValue({ passTest: 1000 } as any); const collectors = new CollectorSet({ logger }); collectors.registerCollector( new Collector(logger, { type: 'MY_TEST_COLLECTOR', fetch: (collectorFetchContext: any) => { - return collectorFetchContext.callCluster(); + return collectorFetchContext.esClient.get(); }, isReady: () => true, }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); expect(loggerSpies.debug).toHaveBeenCalledTimes(1); expect(loggerSpies.debug).toHaveBeenCalledWith( 'Fetching data from MY_TEST_COLLECTOR collector' @@ -119,7 +119,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); } catch (err) { // Do nothing } @@ -137,7 +137,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -155,7 +155,7 @@ describe('CollectorSet', () => { } as any) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 3555b05518fdb..fc47bbcd16649 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -20,7 +20,6 @@ import { snakeCase } from 'lodash'; import { Logger, - LegacyAPICaller, ElasticsearchClient, ISavedObjectsRepository, SavedObjectsClientContract, @@ -171,7 +170,6 @@ export class CollectorSet { }; public bulkFetch = async ( - callCluster: LegacyAPICaller, esClient: ElasticsearchClient, soClient: SavedObjectsClientContract | ISavedObjectsRepository, kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter @@ -182,7 +180,6 @@ export class CollectorSet { this.logger.debug(`Fetching data from ${collector.type} collector`); try { const context = { - callCluster, esClient, soClient, ...(collector.extendFetchContext.kibanaRequest && { kibanaRequest }), @@ -212,14 +209,12 @@ export class CollectorSet { }; public bulkFetchUsage = async ( - callCluster: LegacyAPICaller, esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository, kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); return await this.bulkFetch( - callCluster, esClient, savedObjectsClient, kibanaRequest, diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index 16a1c2c742f04..bbb4c94e02d5f 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -28,7 +28,6 @@ import { IRouter, ISavedObjectsRepository, KibanaRequest, - LegacyAPICaller, MetricsServiceSetup, SavedObjectsClientContract, ServiceStatus, @@ -66,17 +65,11 @@ export function registerStatsRoute({ overallStatus$: Observable; }) { const getUsage = async ( - callCluster: LegacyAPICaller, esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository, kibanaRequest: KibanaRequest ): Promise => { - const usage = await collectorSet.bulkFetchUsage( - callCluster, - esClient, - savedObjectsClient, - kibanaRequest - ); + const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient, kibanaRequest); return collectorSet.toObject(usage); }; @@ -110,7 +103,6 @@ export function registerStatsRoute({ let extended; if (isExtended) { - const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const { asCurrentUser } = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; @@ -122,7 +114,7 @@ export function registerStatsRoute({ } const usagePromise = shouldGetUsage - ? getUsage(callCluster, asCurrentUser, savedObjectsClient, req) + ? getUsage(asCurrentUser, savedObjectsClient, req) : Promise.resolve({}); const [usage, clusterUuid] = await Promise.all([ usagePromise, diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index fb0a2e56ff3c9..1295572335a66 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -50,7 +50,6 @@ export const createUsageCollectionSetupMock = () => { export function createCollectorFetchContextMock(): jest.Mocked> { const collectorFetchClientsMock: jest.Mocked> = { - callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsRepositoryMock.create(), }; @@ -61,7 +60,6 @@ export function createCollectorFetchContextWithKibanaMock(): jest.Mocked< CollectorFetchContext > { const collectorFetchClientsMock: jest.Mocked> = { - callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsRepositoryMock.create(), kibanaRequest: httpServerMock.createKibanaRequest(), diff --git a/src/plugins/vis_default_editor/kibana.json b/src/plugins/vis_default_editor/kibana.json index 35ad0a3a8be9a..9664b14821c0d 100644 --- a/src/plugins/vis_default_editor/kibana.json +++ b/src/plugins/vis_default_editor/kibana.json @@ -2,5 +2,6 @@ "id": "visDefaultEditor", "version": "kibana", "ui": true, + "optionalPlugins": ["visualize"], "requiredBundles": ["kibanaUtils", "kibanaReact", "data"] } diff --git a/src/plugins/vis_default_editor/public/components/agg.test.tsx b/src/plugins/vis_default_editor/public/components/agg.test.tsx index db107fa589083..704037fd35a41 100644 --- a/src/plugins/vis_default_editor/public/components/agg.test.tsx +++ b/src/plugins/vis_default_editor/public/components/agg.test.tsx @@ -22,11 +22,11 @@ import { mount, shallow } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { IndexPattern, IAggType, AggGroupNames } from 'src/plugins/data/public'; +import type { Schema } from '../../../visualizations/public'; import { DefaultEditorAgg, DefaultEditorAggProps } from './agg'; import { DefaultEditorAggParams } from './agg_params'; import { AGGS_ACTION_KEYS } from './agg_group_state'; -import { Schema } from '../schemas'; import { EditorVisState } from './sidebar/state/reducers'; jest.mock('./agg_params', () => ({ diff --git a/src/plugins/vis_default_editor/public/components/agg_add.tsx b/src/plugins/vis_default_editor/public/components/agg_add.tsx index 2da7b33139a8e..50913d2058489 100644 --- a/src/plugins/vis_default_editor/public/components/agg_add.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_add.tsx @@ -30,7 +30,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { IAggConfig, AggGroupNames } from '../../../data/public'; -import { Schema } from '../schemas'; +import type { Schema } from '../../../visualizations/public'; interface DefaultEditorAggAddProps { group?: IAggConfig[]; diff --git a/src/plugins/vis_default_editor/public/components/agg_common_props.ts b/src/plugins/vis_default_editor/public/components/agg_common_props.ts index 40d7b79bfbefc..998a767d841ec 100644 --- a/src/plugins/vis_default_editor/public/components/agg_common_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_common_props.ts @@ -17,10 +17,10 @@ * under the License. */ -import { VisParams } from 'src/plugins/visualizations/public'; -import { IAggType, IAggConfig, AggGroupName } from 'src/plugins/data/public'; -import { Schema } from '../schemas'; -import { EditorVisState } from './sidebar/state/reducers'; +import type { VisParams, Schema } from 'src/plugins/visualizations/public'; +import type { IAggType, IAggConfig, AggGroupName } from 'src/plugins/data/public'; + +import type { EditorVisState } from './sidebar/state/reducers'; type AggId = IAggConfig['id']; type AggParams = IAggConfig['params']; diff --git a/src/plugins/vis_default_editor/public/components/agg_group.test.tsx b/src/plugins/vis_default_editor/public/components/agg_group.test.tsx index 483446daed10f..86b17b0e160bd 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.test.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.test.tsx @@ -20,21 +20,28 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { act } from 'react-dom/test-utils'; -import { IAggConfigs, IAggConfig } from 'src/plugins/data/public'; +import type { IAggConfigs, IAggConfig } from 'src/plugins/data/public'; +import { ISchemas } from 'src/plugins/visualizations/public'; +import { createMockedVisEditorSchemas } from 'src/plugins/visualizations/public/mocks'; + import { DefaultEditorAggGroup, DefaultEditorAggGroupProps } from './agg_group'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; -import { ISchemas, Schemas } from '../schemas'; -import { EditorVisState } from './sidebar/state/reducers'; - -jest.mock('@elastic/eui', () => ({ - EuiTitle: 'eui-title', - EuiDragDropContext: 'eui-drag-drop-context', - EuiDroppable: 'eui-droppable', - EuiDraggable: (props: any) => props.children({ dragHandleProps: {} }), - EuiSpacer: 'eui-spacer', - EuiPanel: 'eui-panel', -})); +import type { EditorVisState } from './sidebar/state/reducers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiTitle: 'eui-title', + EuiDragDropContext: 'eui-drag-drop-context', + EuiDroppable: 'eui-droppable', + EuiDraggable: (props: any) => props.children({ dragHandleProps: {} }), + EuiSpacer: 'eui-spacer', + EuiPanel: 'eui-panel', + }; +}); jest.mock('./agg', () => ({ DefaultEditorAgg: () =>
, @@ -56,7 +63,7 @@ describe('DefaultEditorAgg component', () => { setTouched = jest.fn(); setValidity = jest.fn(); reorderAggs = jest.fn(); - schemas = new Schemas([ + schemas = createMockedVisEditorSchemas([ { name: 'metrics', group: 'metrics', diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index 4cde33b8fbc31..76bc9ca881b98 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -31,6 +31,7 @@ import { import { i18n } from '@kbn/i18n'; import { AggGroupNames, AggGroupLabels, IAggConfig, TimeRange } from '../../../data/public'; +import type { Schema } from '../../../visualizations/public'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from './agg_common_props'; @@ -41,7 +42,6 @@ import { getEnabledMetricAggsCount, } from './agg_group_helper'; import { aggGroupReducer, initAggsState, AGGS_ACTION_KEYS } from './agg_group_state'; -import { Schema } from '../schemas'; export interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps { schemas: Schema[]; diff --git a/src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts index 3693f1b1e3091..821796d417278 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts +++ b/src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts @@ -17,7 +17,9 @@ * under the License. */ -import { IAggConfig } from 'src/plugins/data/public'; +import type { IAggConfig } from 'src/plugins/data/public'; +import type { Schema } from 'src/plugins/visualizations/public'; + import { isAggRemovable, calcAggIsTooLow, @@ -25,7 +27,6 @@ import { getEnabledMetricAggsCount, } from './agg_group_helper'; import { AggsState } from './agg_group_state'; -import { Schema } from '../schemas'; describe('DefaultEditorGroup helpers', () => { let group: IAggConfig[]; diff --git a/src/plugins/vis_default_editor/public/components/agg_group_helper.tsx b/src/plugins/vis_default_editor/public/components/agg_group_helper.tsx index a5a949ce66c82..9fe24e3b52b24 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group_helper.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group_helper.tsx @@ -18,9 +18,11 @@ */ import { findIndex, isEmpty } from 'lodash'; -import { IAggConfig } from 'src/plugins/data/public'; -import { AggsState } from './agg_group_state'; -import { Schema, getSchemaByName } from '../schemas'; +import type { IAggConfig } from 'src/plugins/data/public'; +import type { Schema } from 'src/plugins/visualizations/public'; + +import { getSchemaByName } from '../schemas'; +import type { AggsState } from './agg_group_state'; const isAggRemovable = (agg: IAggConfig, group: IAggConfig[], schemas: Schema[]) => { const schema = getSchemaByName(schemas, agg.schema); diff --git a/src/plugins/vis_default_editor/public/components/agg_param_props.ts b/src/plugins/vis_default_editor/public/components/agg_param_props.ts index 076bddc9551ea..dd6bac6bb25f5 100644 --- a/src/plugins/vis_default_editor/public/components/agg_param_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_param_props.ts @@ -23,9 +23,9 @@ import { IndexPatternField, OptionedValueProp, } from 'src/plugins/data/public'; +import type { Schema } from 'src/plugins/visualizations/public'; import { ComboBoxGroupedOptions } from '../utils'; import { EditorConfig } from './utils'; -import { Schema } from '../schemas'; import { EditorVisState } from './sidebar/state/reducers'; // NOTE: we cannot export the interface with export { InterfaceName } diff --git a/src/plugins/vis_default_editor/public/components/agg_params.tsx b/src/plugins/vis_default_editor/public/components/agg_params.tsx index 78398d8359e9e..662cc641ef7bf 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_params.tsx @@ -23,6 +23,7 @@ import { i18n } from '@kbn/i18n'; import useUnmount from 'react-use/lib/useUnmount'; import { IAggConfig, IndexPattern, AggGroupNames } from '../../../data/public'; +import type { Schema } from '../../../visualizations/public'; import { DefaultEditorAggSelect } from './agg_select'; import { DefaultEditorAggParam } from './agg_param'; @@ -39,7 +40,7 @@ import { } from './agg_params_state'; import { DefaultEditorCommonProps } from './agg_common_props'; import { EditorParamConfig, TimeIntervalParam, FixedParam, getEditorConfig } from './utils'; -import { Schema, getSchemaByName } from '../schemas'; +import { getSchemaByName } from '../schemas'; import { useKibana } from '../../../kibana_react/public'; import { VisDefaultEditorKibanaServices } from '../types'; diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts index a56155db02f6b..df79a48e4f11f 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts @@ -24,6 +24,8 @@ import { IAggType, IndexPattern, } from 'src/plugins/data/public'; +import type { Schema } from 'src/plugins/visualizations/public'; + import { getAggParamsToRender, getAggTypeOptions, @@ -31,7 +33,6 @@ import { } from './agg_params_helper'; import { FieldParamEditor, OrderByParamEditor } from './controls'; import { EditorConfig } from './utils'; -import { Schema } from '../schemas'; import { EditorVisState } from './sidebar/state/reducers'; import { groupAndSortBy } from '../utils'; diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 271fc75a0853e..5b2ccb20e2ac9 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -27,13 +27,15 @@ import { IndexPattern, IndexPatternField, } from '../../../data/public'; +import type { Schema } from '../../../visualizations/public'; + import { filterAggTypes, filterAggTypeFields } from '../agg_filters'; import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; import { AggTypeState, AggParamsState } from './agg_params_state'; import { AggParamEditorProps } from './agg_param_props'; import { aggParamsMap } from './agg_params_map'; import { EditorConfig } from './utils'; -import { Schema, getSchemaByName } from '../schemas'; +import { getSchemaByName } from '../schemas'; import { EditorVisState } from './sidebar/state/reducers'; interface ParamInstanceBase { diff --git a/src/plugins/charts/public/static/components/basic_options.tsx b/src/plugins/vis_default_editor/public/components/options/basic_options.tsx similarity index 86% rename from src/plugins/charts/public/static/components/basic_options.tsx rename to src/plugins/vis_default_editor/public/components/options/basic_options.tsx index 9c5a22543df99..f67a9997cb5e2 100644 --- a/src/plugins/charts/public/static/components/basic_options.tsx +++ b/src/plugins/vis_default_editor/public/components/options/basic_options.tsx @@ -21,8 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; - +import { VisOptionsProps } from '../../vis_options_props'; import { SwitchOption } from './switch'; import { SelectOption } from './select'; @@ -39,7 +38,7 @@ function BasicOptions({ return ( <> ({ setValue={setValue} /> void; @@ -71,7 +71,7 @@ function ColorRanges({ return ( ( paramName: T, @@ -67,7 +66,7 @@ function ColorSchemaOptions({ }} > @@ -80,11 +79,11 @@ function ColorSchemaOptions({ disabled={disabled} helpText={ showHelpText && - i18n.translate('charts.controls.colorSchema.howToChangeColorsDescription', { + i18n.translate('visDefaultEditor.options.colorSchema.howToChangeColorsDescription', { defaultMessage: 'Individual colors can be changed in the legend.', }) } - label={i18n.translate('charts.controls.colorSchema.colorSchemaLabel', { + label={i18n.translate('visDefaultEditor.options.colorSchema.colorSchemaLabel', { defaultMessage: 'Color schema', })} labelAppend={isCustomColors && resetColorsButton} @@ -96,7 +95,7 @@ function ColorSchemaOptions({ ({ const [stateValue, setStateValue] = useState(value); const [isValidState, setIsValidState] = useState(true); - const error = i18n.translate('charts.controls.rangeErrorMessage', { + const error = i18n.translate('visDefaultEditor.options.rangeErrorMessage', { defaultMessage: 'Values must be on or between {min} and {max}', values: { min, max }, }); diff --git a/src/plugins/charts/public/static/components/required_number_input.tsx b/src/plugins/vis_default_editor/public/components/options/required_number_input.tsx similarity index 100% rename from src/plugins/charts/public/static/components/required_number_input.tsx rename to src/plugins/vis_default_editor/public/components/options/required_number_input.tsx diff --git a/src/plugins/charts/public/static/components/select.tsx b/src/plugins/vis_default_editor/public/components/options/select.tsx similarity index 100% rename from src/plugins/charts/public/static/components/select.tsx rename to src/plugins/vis_default_editor/public/components/options/select.tsx diff --git a/src/plugins/charts/public/static/components/switch.tsx b/src/plugins/vis_default_editor/public/components/options/switch.tsx similarity index 100% rename from src/plugins/charts/public/static/components/switch.tsx rename to src/plugins/vis_default_editor/public/components/options/switch.tsx diff --git a/src/plugins/charts/public/static/components/text_input.tsx b/src/plugins/vis_default_editor/public/components/options/text_input.tsx similarity index 100% rename from src/plugins/charts/public/static/components/text_input.tsx rename to src/plugins/vis_default_editor/public/components/options/text_input.tsx diff --git a/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx index b2c7bcafa15a3..8c213c6f77261 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx @@ -28,6 +28,7 @@ import { search, TimeRange, } from '../../../../data/public'; +import type { ISchemas } from '../../../../visualizations/public'; import { DefaultEditorAggGroup } from '../agg_group'; import { EditorAction, @@ -38,9 +39,8 @@ import { changeAggType, toggleEnabledAgg, } from './state'; -import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from '../agg_common_props'; -import { ISchemas } from '../../schemas'; -import { EditorVisState } from './state/reducers'; +import type { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from '../agg_common_props'; +import type { EditorVisState } from './state/reducers'; export interface DefaultEditorDataTabProps { dispatch: React.Dispatch; diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index d6b69a769e0a3..d4116375fd796 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -28,6 +28,7 @@ import { PersistedState, VisualizeEmbeddableContract, } from 'src/plugins/visualizations/public'; +import type { Schema } from 'src/plugins/visualizations/public'; import { TimeRange } from 'src/plugins/data/public'; import { SavedObject } from 'src/plugins/saved_objects/public'; import { DefaultEditorNavBar } from './navbar'; @@ -35,7 +36,6 @@ import { DefaultEditorControls } from './controls'; import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; import { SidebarTitle } from './sidebar_title'; -import { Schema } from '../../schemas'; import { useOptionTabs } from './use_option_tabs'; interface DefaultEditorSideBarProps { diff --git a/src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts index 0e2724ecc08a8..bc02a15c61aeb 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts @@ -17,10 +17,10 @@ * under the License. */ -import { Vis, VisParams } from 'src/plugins/visualizations/public'; -import { IAggConfig } from 'src/plugins/data/public'; +import { Vis, VisParams, Schema } from 'src/plugins/visualizations/public'; +import type { IAggConfig } from 'src/plugins/data/public'; + import { EditorStateActionTypes } from './constants'; -import { Schema } from '../../../schemas'; export interface ActionType { type: T; diff --git a/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts b/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts index 90e2d792d3597..93186b8ffcb9b 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts @@ -35,7 +35,7 @@ export interface OptionTab { export const useOptionTabs = ({ type: visType }: Vis): [OptionTab[], (name: string) => void] => { const [optionTabs, setOptionTabs] = useState(() => { const tabs = [ - ...(visType.schemas.buckets || visType.schemas.metrics + ...(visType.schemas.buckets?.length || visType.schemas.metrics?.length ? [ { name: 'data', diff --git a/src/plugins/vis_default_editor/public/index.ts b/src/plugins/vis_default_editor/public/index.ts index d7eb5eda7bdfe..06834ab19c876 100644 --- a/src/plugins/vis_default_editor/public/index.ts +++ b/src/plugins/vis_default_editor/public/index.ts @@ -17,18 +17,18 @@ * under the License. */ -export { DefaultEditorController } from './default_editor_controller'; +import { PluginInitializerContext } from 'kibana/public'; +import { DefaultEditorController } from './default_editor_controller'; +import { VisDefaultEditorPlugin } from './plugin'; + +export { DefaultEditorController }; export { useValidation } from './components/controls/utils'; +export * from './components/options'; export { RangesParamEditor, RangeValues } from './components/controls/ranges'; export * from './editor_size'; export * from './vis_options_props'; export * from './utils'; -export { ISchemas, Schemas, Schema } from './schemas'; -/** dummy plugin, we just want visDefaultEditor to have its own bundle */ -export function plugin() { - return new (class VisDefaultEditor { - setup() {} - start() {} - })(); -} +export const plugin = (context: PluginInitializerContext) => { + return new VisDefaultEditorPlugin(); +}; diff --git a/src/plugins/vis_default_editor/public/plugin.ts b/src/plugins/vis_default_editor/public/plugin.ts new file mode 100644 index 0000000000000..a7a5c6146a6e8 --- /dev/null +++ b/src/plugins/vis_default_editor/public/plugin.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 { CoreSetup, Plugin } from 'kibana/public'; + +import { VisualizePluginSetup } from '../../visualize/public'; +import { DefaultEditorController } from './default_editor_controller'; + +export interface VisDefaultEditorSetupDependencies { + visualize: VisualizePluginSetup; +} + +export class VisDefaultEditorPlugin + implements Plugin { + public setup(core: CoreSetup, { visualize }: VisDefaultEditorSetupDependencies) { + if (visualize) { + visualize.setDefaultEditor(DefaultEditorController); + } + } + + public start() {} + + stop() {} +} diff --git a/src/plugins/vis_default_editor/public/schemas.ts b/src/plugins/vis_default_editor/public/schemas.ts index 7ecb4e54726b8..204f2eb68bec6 100644 --- a/src/plugins/vis_default_editor/public/schemas.ts +++ b/src/plugins/vis_default_editor/public/schemas.ts @@ -17,86 +17,7 @@ * under the License. */ -import { ReactNode } from 'react'; -import _, { defaults } from 'lodash'; - -import { Optional } from '@kbn/utility-types'; - -import { AggGroupNames, AggParam, AggGroupName } from '../../data/public'; - -export interface ISchemas { - [AggGroupNames.Buckets]: Schema[]; - [AggGroupNames.Metrics]: Schema[]; - all: Schema[]; -} - -export interface Schema { - aggFilter: string[]; - editor: boolean | string; - group: AggGroupName; - max: number; - min: number; - name: string; - params: AggParam[]; - title: string; - defaults: unknown; - hideCustomLabel?: boolean; - mustBeFirst?: boolean; - aggSettings?: any; - disabled?: boolean; - tooltip?: ReactNode; -} - -export class Schemas implements ISchemas { - all: Schema[] = []; - [AggGroupNames.Buckets]: Schema[] = []; - [AggGroupNames.Metrics]: Schema[] = []; - - constructor( - schemas: Array< - Optional< - Schema, - 'min' | 'max' | 'group' | 'title' | 'aggFilter' | 'editor' | 'params' | 'defaults' - > - > - ) { - _(schemas || []) - .chain() - .map((schema) => { - if (!schema.name) throw new Error('all schema must have a unique name'); - - if (schema.name === 'split') { - schema.params = [ - { - name: 'row', - default: true, - }, - ] as AggParam[]; - } - - defaults(schema, { - min: 0, - max: Infinity, - group: AggGroupNames.Buckets, - title: schema.name, - aggFilter: '*', - editor: false, - params: [], - }); - - return schema as Schema; - }) - .tap((fullSchemas: Schema[]) => { - this.all = fullSchemas; - }) - .groupBy('group') - .forOwn((group, groupName) => { - // @ts-ignore - this[groupName] = group; - }) - .commit(); - } -} +import type { Schema } from '../../visualizations/public'; export const getSchemaByName = (schemas: Schema[], schemaName?: string) => { return schemas.find((s) => s.name === schemaName) || ({} as Schema); diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json index c0afcb0e99d13..6cfedf60687ef 100644 --- a/src/plugins/vis_type_markdown/kibana.json +++ b/src/plugins/vis_type_markdown/kibana.json @@ -4,5 +4,5 @@ "ui": true, "server": true, "requiredPlugins": ["expressions", "visualizations"], - "requiredBundles": ["kibanaReact", "charts", "visualizations", "expressions", "visDefaultEditor"] + "requiredBundles": ["kibanaReact", "visualizations", "expressions", "visDefaultEditor"] } diff --git a/src/plugins/vis_type_markdown/public/__snapshots__/markdown_options.test.tsx.snap b/src/plugins/vis_type_markdown/public/__snapshots__/markdown_options.test.tsx.snap new file mode 100644 index 0000000000000..03431fe2437b7 --- /dev/null +++ b/src/plugins/vis_type_markdown/public/__snapshots__/markdown_options.test.tsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MarkdownOptions should match snapshot 1`] = ` + + + + + + +

+ +

+
+
+ + + + + + + +
+
+ + + +
+
+`; diff --git a/src/plugins/vis_type_markdown/public/markdown_options.test.tsx b/src/plugins/vis_type_markdown/public/markdown_options.test.tsx new file mode 100644 index 0000000000000..170dde7ee91e6 --- /dev/null +++ b/src/plugins/vis_type_markdown/public/markdown_options.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { MarkdownVisParams } from './types'; +import { MarkdownOptions } from './markdown_options'; + +describe('MarkdownOptions', () => { + const props = ({ + stateParams: { + fontSize: 12, + markdown: 'hello from 2020 🥳', + openLinksInNewTab: false, + }, + setValue: jest.fn(), + } as unknown) as VisOptionsProps; + + it('should match snapshot', () => { + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should update markdown on change', () => { + const comp = shallow(); + const value = 'see you in 2021 😎'; + const textArea = comp.find('EuiTextArea'); + const onChange = textArea.prop('onChange'); + onChange?.({ + target: { + // @ts-expect-error + value, + }, + }); + + expect(props.setValue).toHaveBeenCalledWith('markdown', value); + }); +}); diff --git a/src/plugins/vis_type_markdown/public/markdown_options.tsx b/src/plugins/vis_type_markdown/public/markdown_options.tsx index a6349793619a0..674fbeb517510 100644 --- a/src/plugins/vis_type_markdown/public/markdown_options.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_options.tsx @@ -22,7 +22,6 @@ import { EuiPanel, EuiTitle, EuiLink, - EuiIcon, EuiTextArea, EuiFlexGroup, EuiFlexItem, @@ -35,7 +34,7 @@ import { MarkdownVisParams } from './types'; function MarkdownOptions({ stateParams, setValue }: VisOptionsProps) { const onMarkdownUpdate = useCallback( - (value: MarkdownVisParams['markdown']) => setValue('markdown', value), + ({ target: { value } }: React.ChangeEvent) => setValue('markdown', value), [setValue] ); @@ -61,8 +60,7 @@ function MarkdownOptions({ stateParams, setValue }: VisOptionsProps{' '} - + /> @@ -74,7 +72,7 @@ function MarkdownOptions({ stateParams, setValue }: VisOptionsProps onMarkdownUpdate(value)} + onChange={onMarkdownUpdate} fullWidth={true} data-test-subj="markdownTextarea" resize="none" diff --git a/src/plugins/vis_type_markdown/public/settings_options.tsx b/src/plugins/vis_type_markdown/public/settings_options.tsx index bf4570db5d4a0..1b793ca573f82 100644 --- a/src/plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/plugins/vis_type_markdown/public/settings_options.tsx @@ -21,8 +21,7 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { RangeOption, SwitchOption } from '../../charts/public'; +import { VisOptionsProps, SwitchOption, RangeOption } from '../../vis_default_editor/public'; import { MarkdownVisParams } from './types'; function SettingsOptions({ stateParams, setValue }: VisOptionsProps) { diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx index d87a0da740d75..58c486dfa90ab 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -29,16 +29,16 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { - ColorMode, ColorRanges, - ColorSchemaOptions, + SetColorRangeValue, + VisOptionsProps, SwitchOption, - RangeOption, SetColorSchemaOptionsValue, - SetColorRangeValue, -} from '../../../charts/public'; + ColorSchemaOptions, + RangeOption, +} from '../../../vis_default_editor/public'; +import { ColorMode } from '../../../charts/public'; import { MetricVisParam, VisParams } from '../types'; function MetricVisOptions({ diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_type_metric/public/metric_vis_type.ts index ba8f27b9412a2..75607de810c63 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.ts @@ -18,11 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; import { MetricVisOptions } from './components/metric_vis_options'; import { ColorSchemas, colorSchemas, ColorMode } from '../../charts/public'; +import { BaseVisTypeOptions } from '../../visualizations/public'; import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; import { toExpressionAst } from './to_ast'; export const createMetricVisTypeDefinition = (): BaseVisTypeOptions => ({ @@ -83,7 +82,7 @@ export const createMetricVisTypeDefinition = (): BaseVisTypeOptions => ({ colorSchemas, }, optionsTemplate: MetricVisOptions, - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -120,6 +119,6 @@ export const createMetricVisTypeDefinition = (): BaseVisTypeOptions => ({ max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], }, - ]), + ], }, }); diff --git a/src/plugins/vis_type_table/kibana.json b/src/plugins/vis_type_table/kibana.json index dce9bce0e8886..1fb8516851ebd 100644 --- a/src/plugins/vis_type_table/kibana.json +++ b/src/plugins/vis_type_table/kibana.json @@ -13,7 +13,6 @@ "kibanaUtils", "kibanaReact", "share", - "charts", "visDefaultEditor" ], "optionalPlugins": ["usageCollection"] diff --git a/src/plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx index b81f0425011da..3932db1262acf 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -23,9 +23,13 @@ import { EuiIconTip, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { search } from '../../../data/public'; -import { SwitchOption, SelectOption, NumberInputOption } from '../../../charts/public'; +import { + SwitchOption, + SelectOption, + NumberInputOption, + VisOptionsProps, +} from '../../../vis_default_editor/public'; import { TableVisParams } from '../types'; import { totalAggregations } from './utils'; diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts index 5aef3fc26fa6c..367c120dc8888 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts @@ -18,7 +18,6 @@ */ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../../data/public'; -import { Schemas } from '../../../vis_default_editor/public'; import { BaseVisTypeOptions } from '../../../visualizations/public'; import { TableOptions } from '../components/table_vis_options_lazy'; @@ -54,7 +53,7 @@ export const tableVisLegacyTypeDefinition: BaseVisTypeOptions = }, editorConfig: { optionsTemplate: TableOptions, - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -88,7 +87,7 @@ export const tableVisLegacyTypeDefinition: BaseVisTypeOptions = max: 1, aggFilter: ['!filter'], }, - ]), + ], }, toExpressionAst, hierarchicalData: (vis) => vis.params.showPartialRows || vis.params.showMetricsAtAllLevels, diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index bfe1427d38496..b15a1a5c03ce3 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -18,7 +18,6 @@ */ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; import { TableOptions } from './components/table_vis_options_lazy'; @@ -51,7 +50,7 @@ export const tableVisTypeDefinition: BaseVisTypeOptions = { }, editorConfig: { optionsTemplate: TableOptions, - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -85,7 +84,7 @@ export const tableVisTypeDefinition: BaseVisTypeOptions = { max: 1, aggFilter: ['!filter'], }, - ]), + ], }, toExpressionAst, hierarchicalData: (vis) => vis.params.showPartialRows || vis.params.showMetricsAtAllLevels, diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index d33576e4e5529..5d5f499d650b4 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,9 +20,8 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from '../../../vis_default_editor/public'; +import { VisOptionsProps, SelectOption, SwitchOption } from '../../../vis_default_editor/public'; import { ValidatedDualRange } from '../../../kibana_react/public'; -import { SelectOption, SwitchOption } from '../../../charts/public'; import { TagCloudVisParams } from '../types'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 71d4408ddc767..8332f7a6615a6 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -18,8 +18,6 @@ */ import { i18n } from '@kbn/i18n'; - -import { Schemas } from '../../vis_default_editor/public'; import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; import { TagCloudOptions } from './components/tag_cloud_options'; @@ -89,7 +87,7 @@ export const tagCloudVisTypeDefinition = { ], }, optionsTemplate: TagCloudOptions, - schemas: new Schemas([ + schemas: [ { group: 'metrics', name: 'metric', @@ -118,6 +116,6 @@ export const tagCloudVisTypeDefinition = { max: 1, aggFilter: ['terms', 'significant_terms'], }, - ]), + ], }, }; diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index bba086720da0a..3ed9aaaaea226 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -19,6 +19,7 @@ import { IRouter, KibanaRequest } from 'kibana/server'; import { schema } from '@kbn/config-schema'; +import { ensureNoUnsafeProperties } from '@kbn/std'; import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; import { visPayloadSchema } from '../../common/vis_schema'; import { ROUTES } from '../../common/constants'; @@ -40,6 +41,14 @@ export const visDataRoutes = ( }, }, async (requestContext, request, response) => { + try { + ensureNoUnsafeProperties(request.body); + } catch (error) { + return response.badRequest({ + body: error.message, + }); + } + try { visPayloadSchema.validate(request.body); } catch (error) { diff --git a/src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx index 0bd5694f71021..e02f62fa6ed6b 100644 --- a/src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx @@ -21,8 +21,7 @@ import React from 'react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { SwitchOption, TextInputOption } from '../../../../../charts/public'; +import { SwitchOption, TextInputOption } from '../../../../../vis_default_editor/public'; import { GaugeOptionsInternalProps } from '../gauge'; function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInternalProps) { diff --git a/src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx index c297fb08e4b68..ec5201af2e7d0 100644 --- a/src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx @@ -21,15 +21,13 @@ import React, { useCallback } from 'react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - import { ColorRanges, - ColorSchemaOptions, - ColorSchemaParams, SetColorRangeValue, SwitchOption, - ColorSchemas, -} from '../../../../../charts/public'; + ColorSchemaOptions, +} from '../../../../../vis_default_editor/public'; +import { ColorSchemaParams, ColorSchemas } from '../../../../../charts/public'; import { GaugeOptionsInternalProps } from '../gauge'; import { Gauge } from '../../../gauge'; diff --git a/src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx index b299b2e86ca40..9cb807aac5759 100644 --- a/src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SelectOption } from '../../../../../charts/public'; +import { SelectOption } from '../../../../../vis_default_editor/public'; import { GaugeOptionsInternalProps } from '../gauge'; import { AggGroupNames } from '../../../../../data/public'; diff --git a/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx b/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx index f5b853accb08e..a409762b30f9f 100644 --- a/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx @@ -23,18 +23,18 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; import { ValueAxis } from '../../../../../vis_type_xy/public'; import { + VisOptionsProps, BasicOptions, - ColorRanges, - ColorSchemaOptions, - NumberInputOption, SelectOption, SwitchOption, - SetColorSchemaOptionsValue, + ColorRanges, SetColorRangeValue, -} from '../../../../../charts/public'; + SetColorSchemaOptionsValue, + ColorSchemaOptions, + NumberInputOption, +} from '../../../../../vis_default_editor/public'; import { HeatmapVisParams } from '../../../heatmap'; import { LabelsPanel } from './labels_panel'; diff --git a/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx index 8ec06ea50ec12..506e5f74dc972 100644 --- a/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx @@ -23,8 +23,7 @@ import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; -import { SwitchOption } from '../../../../../charts/public'; +import { VisOptionsProps, SwitchOption } from '../../../../../vis_default_editor/public'; import { ValueAxis } from '../../../../../vis_type_xy/public'; import { HeatmapVisParams } from '../../../heatmap'; diff --git a/src/plugins/vis_type_vislib/public/editor/components/pie.tsx b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx index 1c3aa501b4d00..01516630287ec 100644 --- a/src/plugins/vis_type_vislib/public/editor/components/pie.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx @@ -22,8 +22,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; -import { BasicOptions, SwitchOption } from '../../../../charts/public'; +import { BasicOptions, SwitchOption, VisOptionsProps } from '../../../../vis_default_editor/public'; import { TruncateLabelsOption } from '../../../../vis_type_xy/public'; import { PieVisParams } from '../../pie'; diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index de32ee17a21bf..ef34399b1d851 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { ColorMode, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../charts/public'; -import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { RangeValues } from '../../vis_default_editor/public'; import { AggGroupNames } from '../../data/public'; import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; @@ -115,7 +115,7 @@ export const gaugeVisTypeDefinition: BaseVisTypeOptions = { editorConfig: { collections: getGaugeCollections(), optionsTemplate: GaugeOptions, - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -144,7 +144,7 @@ export const gaugeVisTypeDefinition: BaseVisTypeOptions = { max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], }, - ]), + ], }, useCustomNoDataScreen: true, }; diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index 56361421261fc..3b8153048a861 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; import { ColorMode, ColorSchemas } from '../../charts/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; @@ -79,7 +78,7 @@ export const goalVisTypeDefinition: BaseVisTypeOptions = { editorConfig: { collections: getGaugeCollections(), optionsTemplate: GaugeOptions, - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -108,7 +107,7 @@ export const goalVisTypeDefinition: BaseVisTypeOptions = { max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], }, - ]), + ], }, useCustomNoDataScreen: true, }; diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index 4a815fd8b2c73..c8eeff406b7fa 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { Position } from '@elastic/charts'; -import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { RangeValues } from '../../vis_default_editor/public'; import { AggGroupNames } from '../../data/public'; import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; import { VIS_EVENT_TO_TRIGGER, BaseVisTypeOptions } from '../../visualizations/public'; @@ -88,7 +88,7 @@ export const heatmapVisTypeDefinition: BaseVisTypeOptions = { editorConfig: { collections: getHeatmapCollections(), optionsTemplate: HeatmapOptions, - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -136,6 +136,6 @@ export const heatmapVisTypeDefinition: BaseVisTypeOptions = { max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], }, - ]), + ], }, }; diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index aa5a3ceaaba98..a58784405541f 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Position } from '@elastic/charts'; import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; import { getPositions } from '../../vis_type_xy/public'; @@ -69,7 +68,7 @@ export const pieVisTypeDefinition: BaseVisTypeOptions = { legendPositions: getPositions(), }, optionsTemplate: PieOptions, - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -102,7 +101,7 @@ export const pieVisTypeDefinition: BaseVisTypeOptions = { max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], }, - ]), + ], }, hierarchicalData: true, responseHandler: 'vislib_slices', diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index 36a184d3da507..0f849c1833230 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -61,7 +61,7 @@ export class VisTypeVislibPlugin core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { - if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, true)) { + if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { // Register only non-replaced vis types convertedTypeDefinitions.forEach(visualizations.createBaseVisualization); visualizations.createBaseVisualization(pieVisTypeDefinition); diff --git a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx index b8dbd0f945c32..e2e8a98a9a8b6 100644 --- a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx +++ b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx @@ -61,7 +61,7 @@ const VislibWrapper = ({ core, charts, visData, visConfig, handlers }: VislibWra visController.current?.destroy(); visController.current = null; }; - }, [core, charts, handlers]); + }, [core, charts]); useEffect(updateChart, [updateChart]); diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json index 14c3ce36bf375..619fa8e71c0dd 100644 --- a/src/plugins/vis_type_xy/kibana.json +++ b/src/plugins/vis_type_xy/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["charts", "data", "expressions", "visualizations"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"] } diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx index a551163747526..d4647ae41a637 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx @@ -24,8 +24,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { Position } from '@elastic/charts'; -import { SelectOption, SwitchOption } from '../../../../../../charts/public'; -import { VisOptionsProps } from '../../../../../../vis_default_editor/public'; +import { + SelectOption, + SwitchOption, + VisOptionsProps, +} from '../../../../../../vis_default_editor/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CategoryAxis } from '../../../../types'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx index c379fa30b49b8..070d5fe018150 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { Vis } from '../../../../../../visualizations/public'; -import { SelectOption } from '../../../../../../charts/public'; +import { SelectOption } from '../../../../../../vis_default_editor/public'; import { SeriesParam, ValueAxis } from '../../../../types'; import { LineOptions } from './line_options'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx index 86a0c56e46942..f64bdba542b99 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx @@ -21,7 +21,7 @@ import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { NumberInputOption, SwitchOption } from '../../../../../../charts/public'; +import { NumberInputOption, SwitchOption } from '../../../../../../vis_default_editor/public'; import { ValueAxis } from '../../../../types'; import { YExtents } from './y_extents'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/label_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/label_options.tsx index 8c5c440ad9de9..bc00e3768aed6 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/label_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/label_options.tsx @@ -23,7 +23,8 @@ import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SelectOption, SwitchOption, Labels } from '../../../../../../charts/public'; +import { SelectOption, SwitchOption } from '../../../../../../vis_default_editor/public'; +import { Labels } from '../../../../../../charts/public'; import { TruncateLabelsOption } from '../../common'; import { getRotateOptions } from '../../../collections'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx index 7727f90f79107..c4a8fea510f82 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { NumberInputOption } from '../../../../../../charts/public'; +import { NumberInputOption } from '../../../../../../vis_default_editor/public'; import { LineOptions, LineOptionsParams } from './line_options'; import { seriesParam, vis } from './mocks'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx index df2735396b38d..39a2ad8de95fd 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx @@ -23,7 +23,11 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { Vis } from '../../../../../../visualizations/public'; -import { NumberInputOption, SelectOption, SwitchOption } from '../../../../../../charts/public'; +import { + NumberInputOption, + SelectOption, + SwitchOption, +} from '../../../../../../vis_default_editor/public'; import { SeriesParam } from '../../../../types'; import { SetChart } from './chart_options'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx index 0b325198c3fe7..62757d14a0196 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx @@ -22,7 +22,7 @@ import { shallow } from 'enzyme'; import { Position } from '@elastic/charts'; -import { TextInputOption } from '../../../../../../charts/public'; +import { TextInputOption } from '../../../../../../vis_default_editor/public'; import { ValueAxis, ScaleType } from '../../../../types'; import { LabelOptions } from './label_options'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx index 4ab792142e83a..d81ddcb95ce62 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx @@ -22,7 +22,11 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; import { Vis } from '../../../../../../visualizations/public'; -import { SelectOption, SwitchOption, TextInputOption } from '../../../../../../charts/public'; +import { + SelectOption, + SwitchOption, + TextInputOption, +} from '../../../../../../vis_default_editor/public'; import { ValueAxis } from '../../../../types'; import { LabelOptions, SetAxisLabel } from './label_options'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx index c2af7f2ad921b..27a28d96d0608 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx @@ -20,10 +20,9 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; -import { NumberInputOption } from '../../../../../../charts/public'; - import { ScaleType } from '../../../../types'; import { YExtents, YExtentsProps } from './y_extents'; +import { NumberInputOption } from '../../../../../../vis_default_editor/public'; describe('YExtents component', () => { let setMultipleValidity: jest.Mock; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx index 11d049d4864a7..ba7049e984573 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx @@ -21,7 +21,7 @@ import React, { useEffect, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { NumberInputOption } from '../../../../../../charts/public'; +import { NumberInputOption } from '../../../../../../vis_default_editor/public'; import { Scale, ScaleType } from '../../../../types'; import { SetScale } from './value_axis_options'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx index 126c5521f0633..a3e573741644c 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -20,14 +20,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; -import { SelectOption, SwitchOption } from '../../../../../../charts/public'; +import { SelectOption, SwitchOption } from '../../../../../../vis_default_editor/public'; import { ChartType } from '../../../../../common'; import { VisParams } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; +import { getTrackUiMetric } from '../../../../services'; export function ElasticChartsOptions(props: ValidationVisOptionsProps) { + const trackUiMetric = getTrackUiMetric(); const { stateParams, setValue, vis, aggs } = props; const hasLineChart = stateParams.seriesParams.some( @@ -49,7 +52,12 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps })} paramName="detailedTooltip" value={stateParams.detailedTooltip} - setValue={setValue} + setValue={(paramName, value) => { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'detailed_tooltip_switched'); + } + setValue(paramName, value); + }} /> {hasLineChart && ( @@ -61,7 +69,12 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps options={vis.type.editorConfig.collections.fittingFunctions} paramName="fittingFunction" value={stateParams.fittingFunction} - setValue={setValue} + setValue={(paramName, value) => { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'fitting_function_selected'); + } + setValue(paramName, value); + }} /> )} diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/grid_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/grid_panel.tsx index c6ad52f7112c9..9efc9b65b19ee 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/grid_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/grid_panel.tsx @@ -23,8 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { SelectOption, SwitchOption } from '../../../../../../charts/public'; - +import { SelectOption, SwitchOption } from '../../../../../../vis_default_editor/public'; import { VisParams, ValueAxis } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx index 283fc28aed46e..1d00f80e0b0d7 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { BasicOptions, SwitchOption } from '../../../../../../charts/public'; +import { BasicOptions, SwitchOption } from '../../../../../../vis_default_editor/public'; import { BUCKET_TYPES } from '../../../../../../data/public'; import { VisParams } from '../../../../types'; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx index ec21a386a5679..8eab0c478e67b 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx @@ -26,8 +26,7 @@ import { SelectOption, SwitchOption, RequiredNumberInputOption, -} from '../../../../../../charts/public'; - +} from '../../../../../../vis_default_editor/public'; import { ValidationVisOptionsProps } from '../../common'; import { VisParams } from '../../../../types'; diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 7425c5f7248ac..ab22ae57ebbdf 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -22,6 +22,7 @@ import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; import { createVisTypeXyVisFn } from './xy_vis_fn'; import { @@ -32,6 +33,7 @@ import { setTimefilter, setUISettings, setDocLinks, + setTrackUiMetric, } from './services'; import { visTypesDefinitions } from './vis_types'; import { LEGACY_CHARTS_LIBRARY } from '../common'; @@ -47,6 +49,7 @@ export interface VisTypeXyPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + usageCollection: UsageCollectionSetup; } /** @internal */ @@ -69,9 +72,9 @@ export class VisTypeXyPlugin > { public async setup( core: VisTypeXyCoreSetup, - { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies + { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { - if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, true)) { + if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { setUISettings(core.uiSettings); setThemeService(charts.theme); setColorsService(charts.legacyColors); @@ -81,6 +84,8 @@ export class VisTypeXyPlugin visTypesDefinitions.forEach(visualizations.createBaseVisualization); } + setTrackUiMetric(usageCollection?.reportUiCounter.bind(usageCollection, 'vis_type_xy')); + return {}; } diff --git a/src/plugins/vis_type_xy/public/services.ts b/src/plugins/vis_type_xy/public/services.ts index 5a72759ecff6c..086cab8fb217a 100644 --- a/src/plugins/vis_type_xy/public/services.ts +++ b/src/plugins/vis_type_xy/public/services.ts @@ -17,6 +17,7 @@ * under the License. */ +import { UiCounterMetricType } from '@kbn/analytics'; import { CoreSetup, DocLinksStart } from '../../../core/public'; import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; @@ -47,3 +48,7 @@ export const [getColorsService, setColorsService] = createGetterSetter< >('xy charts.color'); export const [getDocLinks, setDocLinks] = createGetterSetter('DocLinks'); + +export const [getTrackUiMetric, setTrackUiMetric] = createGetterSetter< + (metricType: UiCounterMetricType, eventName: string | string[]) => void +>('trackUiMetric'); diff --git a/src/plugins/vis_type_xy/public/vis_types/area.tsx b/src/plugins/vis_type_xy/public/vis_types/area.tsx index 9529456f17d55..58423d2f619fa 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/area.tsx @@ -24,7 +24,6 @@ import { i18n } from '@kbn/i18n'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { Fit, Position } from '@elastic/charts'; -import { Schemas } from '../../../vis_default_editor/public'; import { AggGroupNames } from '../../../data/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; @@ -139,7 +138,7 @@ export const getAreaVisTypeDefinition = ( editorConfig: { collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -196,6 +195,6 @@ export const getAreaVisTypeDefinition = ( tooltip: , }), }, - ]), + ], }, }); diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx b/src/plugins/vis_type_xy/public/vis_types/histogram.tsx index 87fcd53729f57..5bc5f1b49e5da 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.tsx @@ -24,7 +24,6 @@ import { i18n } from '@kbn/i18n'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { Position } from '@elastic/charts'; -import { Schemas } from '../../../vis_default_editor/public'; import { AggGroupNames } from '../../../data/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; @@ -142,7 +141,7 @@ export const getHistogramVisTypeDefinition = ( editorConfig: { collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -199,6 +198,6 @@ export const getHistogramVisTypeDefinition = ( tooltip: , }), }, - ]), + ], }, }); diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx index 2806cb8e14983..3029b3dcd6765 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx @@ -24,7 +24,6 @@ import { i18n } from '@kbn/i18n'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { Position } from '@elastic/charts'; -import { Schemas } from '../../../vis_default_editor/public'; import { AggGroupNames } from '../../../data/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; @@ -141,7 +140,7 @@ export const getHorizontalBarVisTypeDefinition = ( editorConfig: { collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -198,6 +197,6 @@ export const getHorizontalBarVisTypeDefinition = ( tooltip: , }), }, - ]), + ], }, }); diff --git a/src/plugins/vis_type_xy/public/vis_types/line.tsx b/src/plugins/vis_type_xy/public/vis_types/line.tsx index 84e4070df495b..e0f83ce649d23 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/line.tsx @@ -24,7 +24,6 @@ import { i18n } from '@kbn/i18n'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { Position, Fit } from '@elastic/charts'; -import { Schemas } from '../../../vis_default_editor/public'; import { AggGroupNames } from '../../../data/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; @@ -139,7 +138,7 @@ export const getLineVisTypeDefinition = ( editorConfig: { collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), - schemas: new Schemas([ + schemas: [ { group: AggGroupNames.Metrics, name: 'metric', @@ -190,6 +189,6 @@ export const getLineVisTypeDefinition = ( tooltip: , }), }, - ]), + ], }, }); diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index b5999535064aa..fafc4052a88fa 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -31,7 +31,7 @@ export const uiSettingsConfig: Record> = { name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', { defaultMessage: 'Legacy charts library', }), - value: true, + value: false, description: i18n.translate( 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 5661acc26fdb6..3956f930758d7 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -49,7 +49,6 @@ import { Vis, SerializedVis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; -import { TriggerId } from '../../../ui_actions/public'; import { SavedObjectAttributes } from '../../../../core/types'; import { SavedVisualizationsLoader } from '../saved_visualizations'; import { VisSavedObject } from '../types'; @@ -414,7 +413,7 @@ export class VisualizeEmbeddable }); }; - public supportedTriggers(): TriggerId[] { + public supportedTriggers(): string[] { return this.vis.type.getSupportedTriggers?.() ?? []; } diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 662350bfc3bd0..854e04325b078 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -37,7 +37,14 @@ export { getSchemas as getVisSchemas } from './legacy/build_pipeline'; /** @public types */ export { VisualizationsSetup, VisualizationsStart }; export { VisGroups } from './vis_types'; -export type { VisTypeAlias, VisType, BaseVisTypeOptions, ReactVisTypeOptions } from './vis_types'; +export type { + VisTypeAlias, + VisType, + BaseVisTypeOptions, + ReactVisTypeOptions, + Schema, + ISchemas, +} from './vis_types'; export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; @@ -57,5 +64,5 @@ export { VisToExpressionAst, } from './types'; export { ExprVisAPIEvents } from './expressions/vis'; -export { VisualizationListItem } from './vis_types/vis_type_alias_registry'; +export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 66399352bea7d..7f60e6b1dc2ff 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -18,7 +18,8 @@ */ import { PluginInitializerContext } from '../../../core/public'; -import { VisualizationsSetup, VisualizationsStart } from './'; +import { Schema, VisualizationsSetup, VisualizationsStart } from './'; +import { Schemas } from './vis_types'; import { VisualizationsPlugin } from './plugin'; import { coreMock, applicationServiceMock } from '../../../core/public/mocks'; import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks'; @@ -86,6 +87,9 @@ const createInstance = async () => { }; }; +export const createMockedVisEditorSchemas = (schemas: Array>) => + new Schemas(schemas); + export const visualizationsPluginMock = { createSetupContract, createStartContract, diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index e52cd4d2b2d56..2088f52428aa7 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -18,9 +18,10 @@ */ import { defaultsDeep } from 'lodash'; -import { ISchemas } from 'src/plugins/vis_default_editor/public'; + import { VisParams } from '../types'; import { VisType, VisTypeOptions, VisGroups } from './types'; +import { Schemas } from './schemas'; interface CommonBaseVisTypeOptions extends Pick< @@ -102,6 +103,7 @@ export class BaseVisType implements VisType public readonly inspectorAdapters; public readonly toExpressionAst; public readonly getInfoMessage; + public readonly schemas; constructor(opts: BaseVisTypeOptions) { if (!opts.icon && !opts.image) { @@ -133,10 +135,8 @@ export class BaseVisType implements VisType this.inspectorAdapters = opts.inspectorAdapters; this.toExpressionAst = opts.toExpressionAst; this.getInfoMessage = opts.getInfoMessage; - } - public get schemas(): ISchemas { - return this.editorConfig?.schemas ?? []; + this.schemas = new Schemas(this.editorConfig?.schemas ?? []); } public get requiresSearch(): boolean { diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index a02ac82c8d122..43de5d1ecce53 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -18,7 +18,8 @@ */ export * from './types_service'; +export { Schemas } from './schemas'; export { VisGroups } from './types'; -export type { VisType } from './types'; +export type { VisType, ISchemas, Schema } from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/schemas.ts b/src/plugins/visualizations/public/vis_types/schemas.ts new file mode 100644 index 0000000000000..f19f57c32d546 --- /dev/null +++ b/src/plugins/visualizations/public/vis_types/schemas.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _, { defaults } from 'lodash'; +import { AggGroupNames, AggParam } from '../../../data/public'; +import type { ISchemas, Schema } from './types'; + +/** @private **/ +export class Schemas implements ISchemas { + all: Schema[] = []; + [AggGroupNames.Buckets]: Schema[] = []; + [AggGroupNames.Metrics]: Schema[] = []; + + constructor(schemas: Array>) { + _(schemas || []) + .chain() + .map((schema) => { + if (!schema.name) throw new Error('all schema must have a unique name'); + + if (schema.name === 'split') { + schema.params = [ + { + name: 'row', + default: true, + }, + ] as AggParam[]; + } + + defaults(schema, { + min: 0, + max: Infinity, + group: AggGroupNames.Buckets, + title: schema.name, + aggFilter: '*', + editor: false, + params: [], + }); + + return schema as Schema; + }) + .tap((fullSchemas: Schema[]) => { + this.all = fullSchemas; + }) + .groupBy('group') + .forOwn((group, groupName) => { + // @ts-ignore + this[groupName] = group; + }) + .commit(); + } +} diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 6ea44dc360559..88a4dad106897 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -18,12 +18,10 @@ */ import { IconType } from '@elastic/eui'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { Adapters } from 'src/plugins/inspector'; -import { IndexPattern } from 'src/plugins/data/public'; import { VisEditorConstructor } from 'src/plugins/visualize/public'; -import { ISchemas } from 'src/plugins/vis_default_editor/public'; -import { TriggerContextMapping } from '../../../ui_actions/public'; +import { IndexPattern, AggGroupNames, AggParam, AggGroupName } from '../../../data/public'; import { Vis, VisParams, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; export interface VisTypeOptions { @@ -40,6 +38,29 @@ export enum VisGroups { AGGBASED = 'aggbased', } +export interface ISchemas { + [AggGroupNames.Buckets]: Schema[]; + [AggGroupNames.Metrics]: Schema[]; + all: Schema[]; +} + +export interface Schema { + aggFilter: string[]; + editor: boolean | string; + group: AggGroupName; + max: number; + min: number; + name: string; + params: AggParam[]; + title: string; + defaults: unknown; + hideCustomLabel?: boolean; + mustBeFirst?: boolean; + aggSettings?: any; + disabled?: boolean; + tooltip?: ReactNode; +} + /** * A visualization type representing one specific type of "classical" * visualizations (i.e. not Lens visualizations). @@ -64,7 +85,7 @@ export interface VisType { /** * If given, it will return the supported triggers for this vis. */ - readonly getSupportedTriggers?: () => Array; + readonly getSupportedTriggers?: () => string[]; /** * Some visualizations are created without SearchSource and may change the used indexes during the visualization configuration. diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index fc5dfd4e123fb..c16ddf436381d 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { TriggerContextMapping } from '../../../ui_actions/public'; +import { SavedObject } from '../../../../core/types/saved_objects'; + +export type VisualizationStage = 'experimental' | 'beta' | 'production'; export interface VisualizationListItem { editUrl: string; @@ -24,11 +26,11 @@ export interface VisualizationListItem { error?: string; icon: string; id: string; - stage: 'experimental' | 'beta' | 'production'; + stage: VisualizationStage; savedObjectType: string; title: string; description?: string; - getSupportedTriggers?: () => Array; + getSupportedTriggers?: () => string[]; typeTitle: string; image?: string; } @@ -36,11 +38,7 @@ export interface VisualizationListItem { export interface VisualizationsAppExtension { docTypes: string[]; searchFields?: string[]; - toListItem: (savedObject: { - id: string; - type: string; - attributes: object; - }) => VisualizationListItem; + toListItem: (savedObject: SavedObject) => VisualizationListItem; } export interface VisTypeAliasPromoTooltip { @@ -59,8 +57,8 @@ export interface VisTypeAlias { description: string; note?: string; disabled?: boolean; - getSupportedTriggers?: () => Array; - stage: 'experimental' | 'beta' | 'production'; + getSupportedTriggers?: () => string[]; + stage: VisualizationStage; appExtensions?: { visualizations: VisualizationsAppExtension; diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index 27229a11cd99f..7f5c7d0dc08a2 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -22,8 +22,7 @@ "kibanaUtils", "kibanaReact", "home", - "discover", - "visDefaultEditor", - "presentationUtil" + "presentationUtil", + "discover" ] } diff --git a/src/plugins/visualize/public/actions/visualize_field_action.ts b/src/plugins/visualize/public/actions/visualize_field_action.ts index e570ed5e49e6a..a825ad9e60641 100644 --- a/src/plugins/visualize/public/actions/visualize_field_action.ts +++ b/src/plugins/visualize/public/actions/visualize_field_action.ts @@ -32,7 +32,7 @@ import { import { VISUALIZE_APP_URL_GENERATOR, VisualizeUrlGeneratorState } from '../url_generator'; import { AGGS_TERMS_SIZE_SETTING } from '../../common/constants'; -export const visualizeFieldAction = createAction({ +export const visualizeFieldAction = createAction({ type: ACTION_VISUALIZE_FIELD, id: ACTION_VISUALIZE_FIELD, getDisplayName: () => diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index e4577cb76ab06..faef910560185 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -45,7 +45,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { useEffect(() => { const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = - services.embeddable.getStateTransfer().getIncomingEditorState() || {}; + services.stateTransferService.getIncomingEditorState() || {}; setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index b13169d4b62ec..2106a395c5e3b 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -46,6 +46,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { services, eventEmitter, isChromeVisible, + originatingApp, visualizationIdFromUrl ); const { appState, hasUnappliedChanges } = useVisualizeAppState( @@ -64,8 +65,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useEffect(() => { - const { originatingApp: value } = - services.embeddable.getStateTransfer().getIncomingEditorState() || {}; + const { originatingApp: value } = services.stateTransferService.getIncomingEditorState() || {}; setOriginatingApp(value); }, [services]); diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 5720eca57e7a5..df7b77d1247bc 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -45,7 +45,7 @@ export const VisualizeListing = () => { savedVisualizations, toastNotifications, visualizations, - embeddable, + stateTransferService, savedObjects, savedObjectsPublic, savedObjectsTagging, @@ -74,7 +74,7 @@ export const VisualizeListing = () => { useMount(() => { // Reset editor state if the visualize listing page is loaded. - embeddable.getStateTransfer().clearEditorState(); + stateTransferService.clearEditorState(); chrome.setBreadcrumbs([ { text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 78ee3ed428503..627d5cd00147b 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -80,7 +80,6 @@ const TopNav = ({ }, [visInstance.embeddableHandler] ); - const stateTransfer = services.embeddable.getStateTransfer(); const savedObjectsClient = services.savedObjects.client; const config = useMemo(() => { @@ -96,10 +95,9 @@ const TopNav = ({ visInstance, stateContainer, visualizationIdFromUrl, - stateTransfer, + stateTransfer: services.stateTransferService, savedObjectsClient, embeddableId, - onAppLeave, }, services ); @@ -117,9 +115,7 @@ const TopNav = ({ visualizationIdFromUrl, services, embeddableId, - stateTransfer, savedObjectsClient, - onAppLeave, ]); const [indexPatterns, setIndexPatterns] = useState( vis.data.indexPattern ? [vis.data.indexPattern] : [] @@ -147,8 +143,9 @@ const TopNav = ({ // Confirm when the user has made any changes to an existing visualizations // or when the user has configured something without saving if ( - ((originatingApp && originatingApp === 'dashboards') || originatingApp === 'canvas') && - (hasUnappliedChanges || hasUnsavedChanges) + originatingApp && + (hasUnappliedChanges || hasUnsavedChanges) && + !services.stateTransferService.isTransferInProgress ) { return actions.confirm( i18n.translate('visualize.confirmModal.confirmTextDescription', { @@ -163,10 +160,11 @@ const TopNav = ({ }); }, [ onAppLeave, - hasUnappliedChanges, + originatingApp, hasUnsavedChanges, + hasUnappliedChanges, visualizeCapabilities.save, - originatingApp, + services.stateTransferService.isTransferInProgress, ]); useEffect(() => { diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index c833745592e41..1729d273e24bc 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -43,7 +43,7 @@ import { } from 'src/plugins/kibana_utils/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; -import { EmbeddableStart } from 'src/plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { EventEmitter } from 'events'; import { DashboardStart } from '../../../dashboard/public'; @@ -95,6 +95,7 @@ export interface EditorRenderProps { } export interface VisualizeServices extends CoreStart { + stateTransferService: EmbeddableStateTransfer; embeddable: EmbeddableStart; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; @@ -128,6 +129,7 @@ export interface SavedVisInstance { export interface ByValueVisInstance { vis: Vis; + savedVis: VisSavedObject; savedSearch?: SavedObject; embeddableHandler: VisualizeEmbeddableContract; } diff --git a/src/plugins/visualize/public/application/utils/breadcrumbs.ts b/src/plugins/visualize/public/application/utils/breadcrumbs.ts index a5c246c539c54..1be3f03165398 100644 --- a/src/plugins/visualize/public/application/utils/breadcrumbs.ts +++ b/src/plugins/visualize/public/application/utils/breadcrumbs.ts @@ -21,16 +21,8 @@ import { i18n } from '@kbn/i18n'; import { VisualizeConstants } from '../visualize_constants'; -const appPrefixes: Record = { - dashboards: { - text: i18n.translate('visualize.dashboard.prefix.breadcrumb', { - defaultMessage: 'Dashboard', - }), - }, -}; - const defaultEditText = i18n.translate('visualize.editor.defaultEditBreadcrumbText', { - defaultMessage: 'Edit', + defaultMessage: 'Edit visualization', }); export function getLandingBreadcrumbs() { @@ -44,9 +36,18 @@ export function getLandingBreadcrumbs() { ]; } -export function getCreateBreadcrumbs() { +export function getCreateBreadcrumbs({ + byValue, + originatingAppName, + redirectToOrigin, +}: { + byValue?: boolean; + originatingAppName?: string; + redirectToOrigin?: () => void; +}) { return [ - ...getLandingBreadcrumbs(), + ...(originatingAppName ? [{ text: originatingAppName, onClick: redirectToOrigin }] : []), + ...(!byValue ? getLandingBreadcrumbs() : []), { text: i18n.translate('visualize.editor.createBreadcrumb', { defaultMessage: 'Create', @@ -55,16 +56,23 @@ export function getCreateBreadcrumbs() { ]; } -export function getBreadcrumbsPrefixedWithApp(originatingApp: string) { - const originatingAppBreadcrumb = appPrefixes[originatingApp]; - return [originatingAppBreadcrumb, ...getLandingBreadcrumbs(), { text: defaultEditText }]; -} - -export function getEditBreadcrumbs(text: string = defaultEditText) { +export function getEditBreadcrumbs( + { + byValue, + originatingAppName, + redirectToOrigin, + }: { + byValue?: boolean; + originatingAppName?: string; + redirectToOrigin?: () => void; + }, + title: string = defaultEditText +) { return [ - ...getLandingBreadcrumbs(), + ...(originatingAppName ? [{ text: originatingAppName, onClick: redirectToOrigin }] : []), + ...(!byValue ? getLandingBreadcrumbs() : []), { - text, + text: title, }, ]; } diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 6ebe65fd960b4..2420c972977b8 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { TopNavMenuData } from 'src/plugins/navigation/public'; -import { AppMountParameters } from 'kibana/public'; import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public'; import { showSaveModal, @@ -55,7 +54,6 @@ interface TopNavConfigParams { stateTransfer: EmbeddableStateTransfer; savedObjectsClient: SavedObjectsClientContract; embeddableId?: string; - onAppLeave: AppMountParameters['onAppLeave']; } export const getTopNavConfig = ( @@ -72,12 +70,10 @@ export const getTopNavConfig = ( visualizationIdFromUrl, stateTransfer, embeddableId, - onAppLeave, }: TopNavConfigParams, { application, chrome, - embeddable, history, share, setActiveUrl, @@ -89,14 +85,11 @@ export const getTopNavConfig = ( }: VisualizeServices ) => { const { vis, embeddableHandler } = visInstance; - const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; + const savedVis = visInstance.savedVis; /** * Called when the user clicks "Save" button. */ async function doSave(saveOptions: SavedObjectSaveOpts) { - if (!savedVis) { - return {}; - } const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave; // vis.title was not bound and it's needed to reflect title into visState stateContainer.transitions.setVis({ @@ -122,15 +115,21 @@ export const getTopNavConfig = ( }); if (originatingApp && saveOptions.returnToOrigin) { - const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`; + if (!embeddableId) { + const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`; - // Manually insert a new url so the back button will open the saved visualization. - history.replace(appPath); - setActiveUrl(appPath); + // Manually insert a new url so the back button will open the saved visualization. + history.replace(appPath); + setActiveUrl(appPath); + } if (newlyCreated && stateTransfer) { stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { - state: { type: VISUALIZE_EMBEDDABLE_TYPE, input: { savedObjectId: id } }, + state: { + type: VISUALIZE_EMBEDDABLE_TYPE, + input: { savedObjectId: id }, + embeddableId, + }, }); } else { application.navigateToApp(originatingApp); @@ -142,7 +141,7 @@ export const getTopNavConfig = ( stateTransfer.clearEditorState(); } chrome.docTitle.change(savedVis.lastSavedTitle); - chrome.setBreadcrumbs(getEditBreadcrumbs(savedVis.lastSavedTitle)); + chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle)); if (id !== visualizationIdFromUrl) { history.replace({ @@ -192,6 +191,24 @@ export const getTopNavConfig = ( } }; + const saveButtonLabel = + embeddableId || + (!savedVis.id && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && originatingApp) + ? i18n.translate('visualize.topNavMenu.saveVisualizationToLibraryButtonLabel', { + defaultMessage: 'Save to library', + }) + : originatingApp && (embeddableId || savedVis.id) + ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { + defaultMessage: 'Save as', + }) + : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { + defaultMessage: 'Save', + }); + + const showSaveAndReturn = + originatingApp && + (savedVis?.id || dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables); + const topNavMenu: TopNavMenuData[] = [ { id: 'inspector', @@ -243,7 +260,7 @@ export const getTopNavConfig = ( // disable the Share button if no action specified disableButton: !share || !!embeddableId, }, - ...(originatingApp === 'dashboards' || originatingApp === 'canvas' + ...(originatingApp ? [ { id: 'cancel', @@ -268,24 +285,16 @@ export const getTopNavConfig = ( }, ] : []), - ...(visualizeCapabilities.save && !embeddableId + ...(visualizeCapabilities.save ? [ { id: 'save', - iconType: savedVis?.id && originatingApp ? undefined : 'save', - label: - savedVis?.id && originatingApp - ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { - defaultMessage: 'save as', - }) - : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { - defaultMessage: 'save', - }), - emphasize: (savedVis && !savedVis.id) || !originatingApp, + iconType: showSaveAndReturn ? undefined : 'save', + label: saveButtonLabel, + emphasize: !showSaveAndReturn, description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', { defaultMessage: 'Save Visualization', }), - className: savedVis?.id && originatingApp ? 'saveAsButton' : '', testId: 'visualizeSaveButton', disableButton: hasUnappliedChanges, tooltip() { @@ -298,7 +307,7 @@ export const getTopNavConfig = ( ); } }, - run: (anchorElement: HTMLElement) => { + run: () => { const onSave = async ({ newTitle, newCopyOnSave, @@ -308,10 +317,6 @@ export const getTopNavConfig = ( returnToOrigin, dashboardId, }: OnSaveProps & { returnToOrigin?: boolean } & { dashboardId?: string | null }) => { - if (!savedVis) { - return; - } - const currentTitle = savedVis.title; savedVis.title = newTitle; embeddableHandler.updateInput({ title: newTitle }); @@ -371,12 +376,10 @@ export const getTopNavConfig = ( let selectedTags: string[] = []; let tagOptions: React.ReactNode | undefined; - if ( - savedVis && - savedObjectsTagging && - savedObjectsTagging.ui.hasTagDecoration(savedVis) - ) { - selectedTags = savedVis.getTags(); + if (savedObjectsTagging) { + if (savedVis && savedObjectsTagging.ui.hasTagDecoration(savedVis)) { + selectedTags = savedVis.getTags(); + } tagOptions = ( {}} originatingApp={originatingApp} + returnToOriginSwitchLabel={ + originatingApp && embeddableId + ? i18n.translate('visualize.topNavMenu.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { + originatingAppName: stateTransfer.getAppNameFromId(originatingApp), + }, + }) + : undefined + } /> ) : ( ); - - const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); - onAppLeave((actions) => { - return actions.default(); - }); - if ( - originatingApp === 'dashboards' && - dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && - !isSaveAsButton - ) { - createVisReference(); - } else if (savedVis) { - showSaveModal(saveModal, I18nContext); - } + showSaveModal(saveModal, I18nContext); }, }, ] : []), - ...(originatingApp && ((savedVis && savedVis.id) || embeddableId) + ...(visualizeCapabilities.save && showSaveAndReturn ? [ { id: 'saveAndReturn', @@ -455,20 +455,13 @@ export const getTopNavConfig = ( } }, run: async () => { + if (!savedVis?.id) { + return createVisReference(); + } const saveOptions = { confirmOverwrite: false, returnToOrigin: true, }; - onAppLeave((actions) => { - return actions.default(); - }); - if ( - originatingApp === 'dashboards' && - dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && - !savedVis - ) { - return createVisReference(); - } return doSave(saveOptions); }, }, diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 6010c4f8b163e..148e2c16c7824 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -71,8 +71,14 @@ export const getVisualizationInstanceFromInput = async ( visualizeServices: VisualizeServices, input: VisualizeInput ) => { - const { visualizations } = visualizeServices; + const { visualizations, savedVisualizations } = visualizeServices; const visState = input.savedVis as SerializedVis; + + /** + * A saved vis is needed even in by value mode to support 'save to library' which converts the 'by value' + * state of the visualization, into a new saved object. + */ + const savedVis: VisSavedObject = await savedVisualizations.get(); let vis = await visualizations.createVis(visState.type, cloneDeep(visState)); if (vis.type.setup) { try { @@ -87,6 +93,7 @@ export const getVisualizationInstanceFromInput = async ( ); return { vis, + savedVis, embeddableHandler, savedSearch, }; diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts index 3f9676a9c9385..48afc9d468204 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -26,6 +26,8 @@ import { redirectWhenMissing } from '../../../../../kibana_utils/public'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; import { VisualizeServices } from '../../types'; import { VisualizeConstants } from '../../visualize_constants'; +import { setDefaultEditor } from '../../../services'; +import { createEmbeddableStateTransferMock } from '../../../../../embeddable/public/mocks'; const mockDefaultEditorControllerDestroy = jest.fn(); const mockEmbeddableHandlerDestroy = jest.fn(); @@ -51,13 +53,17 @@ jest.mock('../get_visualization_instance', () => ({ getVisualizationInstance: jest.fn(() => mockSavedVisInstance), })); jest.mock('../breadcrumbs', () => ({ - getEditBreadcrumbs: jest.fn((text) => text), + getEditBreadcrumbs: jest.fn((args, title) => title), getCreateBreadcrumbs: jest.fn((text) => text), })); -jest.mock('../../../../../vis_default_editor/public', () => ({ - DefaultEditorController: jest.fn(() => ({ destroy: mockDefaultEditorControllerDestroy })), -})); -jest.mock('../../../../../kibana_utils/public'); + +jest.mock('../../../../../kibana_utils/public', () => { + const actual = jest.requireActual('../../../../../kibana_utils/public'); + return { + ...actual, + redirectWhenMissing: jest.fn(), + }; +}); const mockGetVisualizationInstance = jest.requireMock('../get_visualization_instance') .getVisualizationInstance; @@ -69,15 +75,22 @@ describe('useSavedVisInstance', () => { const eventEmitter = new EventEmitter(); beforeEach(() => { + setDefaultEditor( + jest.fn().mockImplementation(() => ({ destroy: mockDefaultEditorControllerDestroy })) + ); + mockServices = ({ ...coreStartMock, toastNotifications, + stateTransferService: createEmbeddableStateTransferMock(), + chrome: { setBreadcrumbs: jest.fn(), docTitle: { change: jest.fn() } }, history: { location: { pathname: VisualizeConstants.EDIT_PATH, }, replace: () => {}, }, + dashboard: { dashboardFeatureFlagConfig: { allowByValueEmbeddables: false } }, visualizations: { all: jest.fn(() => [ { @@ -102,7 +115,7 @@ describe('useSavedVisInstance', () => { test('should not load instance until chrome is defined', () => { const { result } = renderHook(() => - useSavedVisInstance(mockServices, eventEmitter, undefined, undefined) + useSavedVisInstance(mockServices, eventEmitter, undefined, undefined, undefined) ); expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); expect(result.current.visEditorController).toBeUndefined(); @@ -113,7 +126,7 @@ describe('useSavedVisInstance', () => { describe('edit saved visualization route', () => { test('should load instance and initiate an editor if chrome is set up', async () => { const { result, waitForNextUpdate } = renderHook(() => - useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) + useSavedVisInstance(mockServices, eventEmitter, true, undefined, savedVisId) ); result.current.visEditorRef.current = document.createElement('div'); @@ -122,7 +135,11 @@ describe('useSavedVisInstance', () => { await waitForNextUpdate(); expect(mockServices.chrome.setBreadcrumbs).toHaveBeenCalledWith('Test Vis'); - expect(getEditBreadcrumbs).toHaveBeenCalledWith('Test Vis'); + expect(mockServices.chrome.docTitle.change).toHaveBeenCalledWith('Test Vis'); + expect(getEditBreadcrumbs).toHaveBeenCalledWith( + { originatingAppName: undefined, redirectToOrigin: undefined }, + 'Test Vis' + ); expect(getCreateBreadcrumbs).not.toHaveBeenCalled(); expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled(); expect(result.current.visEditorController).toBeDefined(); @@ -131,7 +148,7 @@ describe('useSavedVisInstance', () => { test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { const { result, unmount, waitForNextUpdate } = renderHook(() => - useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) + useSavedVisInstance(mockServices, eventEmitter, true, undefined, savedVisId) ); result.current.visEditorRef.current = document.createElement('div'); @@ -158,7 +175,7 @@ describe('useSavedVisInstance', () => { test('should create new visualization based on search params', async () => { const { result, waitForNextUpdate } = renderHook(() => - useSavedVisInstance(mockServices, eventEmitter, true, undefined) + useSavedVisInstance(mockServices, eventEmitter, true, undefined, undefined) ); result.current.visEditorRef.current = document.createElement('div'); @@ -182,7 +199,7 @@ describe('useSavedVisInstance', () => { search: '?type=myVisType&indexPattern=1a2b3c4d', }; - renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined)); + renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined, undefined)); expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); expect(redirectWhenMissing).toHaveBeenCalled(); @@ -195,7 +212,7 @@ describe('useSavedVisInstance', () => { search: '?type=area', }; - renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined)); + renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined, undefined)); expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); expect(redirectWhenMissing).toHaveBeenCalled(); @@ -206,7 +223,7 @@ describe('useSavedVisInstance', () => { describe('embeded mode', () => { test('should create new visualization based on search params', async () => { const { result, unmount, waitForNextUpdate } = renderHook(() => - useSavedVisInstance(mockServices, eventEmitter, false, savedVisId) + useSavedVisInstance(mockServices, eventEmitter, false, undefined, savedVisId) ); // mock editor ref diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 44fbcce82f458..3f9b3ca9b8b73 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -23,12 +23,12 @@ import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; import { redirectWhenMissing } from '../../../../../kibana_utils/public'; -import { DefaultEditorController } from '../../../../../vis_default_editor/public'; import { getVisualizationInstance } from '../get_visualization_instance'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; import { SavedVisInstance, IEditorController, VisualizeServices } from '../../types'; import { VisualizeConstants } from '../../visualize_constants'; +import { getDefaultEditor } from '../../../services'; /** * This effect is responsible for instantiating a saved vis or creating a new one @@ -38,23 +38,27 @@ export const useSavedVisInstance = ( services: VisualizeServices, eventEmitter: EventEmitter, isChromeVisible: boolean | undefined, + originatingApp: string | undefined, visualizationIdFromUrl: string | undefined ) => { const [state, setState] = useState<{ savedVisInstance?: SavedVisInstance; visEditorController?: IEditorController; }>({}); + const visEditorRef = useRef(null); const visId = useRef(''); useEffect(() => { const { - application: { navigateToApp }, chrome, history, - http: { basePath }, + dashboard, setActiveUrl, toastNotifications, + http: { basePath }, + stateTransferService, + application: { navigateToApp }, } = services; const getSavedVisInstance = async () => { try { @@ -93,18 +97,32 @@ export const useSavedVisInstance = ( const { embeddableHandler, savedVis, vis } = savedVisInstance; + const originatingAppName = originatingApp + ? stateTransferService.getAppNameFromId(originatingApp) + : undefined; + const redirectToOrigin = originatingApp ? () => navigateToApp(originatingApp) : undefined; + const byValueCreateMode = dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; + if (savedVis.id) { - chrome.setBreadcrumbs(getEditBreadcrumbs(savedVis.title)); + chrome.setBreadcrumbs( + getEditBreadcrumbs({ originatingAppName, redirectToOrigin }, savedVis.title) + ); chrome.docTitle.change(savedVis.title); } else { - chrome.setBreadcrumbs(getCreateBreadcrumbs()); + chrome.setBreadcrumbs( + getCreateBreadcrumbs({ + byValue: byValueCreateMode, + originatingAppName, + redirectToOrigin, + }) + ); } let visEditorController; // do not create editor in embeded mode if (visEditorRef.current) { if (isChromeVisible) { - const Editor = vis.type.editor || DefaultEditorController; + const Editor = vis.type.editor || getDefaultEditor(); visEditorController = new Editor( visEditorRef.current, vis, @@ -115,7 +133,6 @@ export const useSavedVisInstance = ( embeddableHandler.render(visEditorRef.current); } } - setState({ savedVisInstance, visEditorController, @@ -172,12 +189,13 @@ export const useSavedVisInstance = ( getSavedVisInstance(); } }, [ + services, eventEmitter, + originatingApp, isChromeVisible, - services, + visualizationIdFromUrl, state.savedVisInstance, state.visEditorController, - visualizationIdFromUrl, ]); useEffect(() => { diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts index e0286a63b9feb..9e222d208f460 100644 --- a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -22,8 +22,8 @@ import { useEffect, useRef, useState } from 'react'; import { VisualizeInput } from 'src/plugins/visualizations/public'; import { ByValueVisInstance, IEditorController, VisualizeServices } from '../../types'; import { getVisualizationInstanceFromInput } from '../get_visualization_instance'; -import { getBreadcrumbsPrefixedWithApp, getEditBreadcrumbs } from '../breadcrumbs'; -import { DefaultEditorController } from '../../../../../vis_default_editor/public'; +import { getEditBreadcrumbs } from '../breadcrumbs'; +import { getDefaultEditor } from '../../../services'; export const useVisByValue = ( services: VisualizeServices, @@ -39,14 +39,19 @@ export const useVisByValue = ( const visEditorRef = useRef(null); const loaded = useRef(false); useEffect(() => { - const { chrome } = services; + const { + chrome, + application: { navigateToApp }, + stateTransferService, + } = services; const getVisInstance = async () => { if (!valueInput || loaded.current || !visEditorRef.current) { return; } const byValueVisInstance = await getVisualizationInstanceFromInput(services, valueInput); const { embeddableHandler, vis } = byValueVisInstance; - const Editor = vis.type.editor || DefaultEditorController; + + const Editor = vis.type.editor || getDefaultEditor(); const visEditorController = new Editor( visEditorRef.current, vis, @@ -54,11 +59,13 @@ export const useVisByValue = ( embeddableHandler ); - if (chrome && originatingApp) { - chrome.setBreadcrumbs(getBreadcrumbsPrefixedWithApp(originatingApp)); - } else if (chrome) { - chrome.setBreadcrumbs(getEditBreadcrumbs()); - } + const originatingAppName = originatingApp + ? stateTransferService.getAppNameFromId(originatingApp) + : undefined; + const redirectToOrigin = originatingApp ? () => navigateToApp(originatingApp) : undefined; + chrome?.setBreadcrumbs( + getEditBreadcrumbs({ byValue: true, originatingAppName, redirectToOrigin }) + ); loaded.current = true; setState({ diff --git a/src/plugins/visualize/public/application/utils/utils.ts b/src/plugins/visualize/public/application/utils/utils.ts index 3d8d443d714a5..779cfb22f7bca 100644 --- a/src/plugins/visualize/public/application/utils/utils.ts +++ b/src/plugins/visualize/public/application/utils/utils.ts @@ -31,7 +31,7 @@ export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksSt links: [ { linkType: 'documentation', - href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/visualize.html`, + href: `${docLinks.links.visualize.guide}`, }, ], }); diff --git a/src/plugins/visualize/public/index.ts b/src/plugins/visualize/public/index.ts index 246806f300800..c9ac85c5123ce 100644 --- a/src/plugins/visualize/public/index.ts +++ b/src/plugins/visualize/public/index.ts @@ -18,7 +18,7 @@ */ import { PluginInitializerContext } from 'kibana/public'; -import { VisualizePlugin } from './plugin'; +import { VisualizePlugin, VisualizePluginSetup } from './plugin'; export type { EditorRenderProps, @@ -27,6 +27,8 @@ export type { } from './application/types'; export { VisualizeConstants } from './application/visualize_constants'; +export { VisualizePluginSetup }; + export const plugin = (context: PluginInitializerContext) => { return new VisualizePlugin(context); }; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index bbd7be0d34883..5eef58a336eab 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -44,7 +44,7 @@ import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/pub import { VisualizationsStart } from '../../visualizations/public'; import { VisualizeConstants } from './application/visualize_constants'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; -import { VisualizeServices } from './application/types'; +import { VisEditorConstructor, VisualizeServices } from './application/types'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { SavedObjectsStart } from '../../saved_objects/public'; import { EmbeddableStart } from '../../embeddable/public'; @@ -57,6 +57,7 @@ import { setIndexPatterns, setQueryService, setShareService, + setDefaultEditor, } from './services'; import { visualizeFieldAction } from './actions/visualize_field_action'; import { createVisualizeUrlGenerator } from './url_generator'; @@ -81,9 +82,18 @@ export interface VisualizePluginSetupDependencies { uiActions: UiActionsSetup; } +export interface VisualizePluginSetup { + setDefaultEditor: (editor: VisEditorConstructor) => void; +} + export class VisualizePlugin implements - Plugin { + Plugin< + VisualizePluginSetup, + void, + VisualizePluginSetupDependencies, + VisualizePluginStartDependencies + > { private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; @@ -192,6 +202,7 @@ export class VisualizePlugin visualizeCapabilities: coreStart.application.capabilities.visualize, visualizations: pluginsStart.visualizations, embeddable: pluginsStart.embeddable, + stateTransferService: pluginsStart.embeddable.getStateTransfer(), setActiveUrl, createVisEmbeddableFromObject: pluginsStart.visualizations.__LEGACY.createVisEmbeddableFromObject, @@ -231,6 +242,12 @@ export class VisualizePlugin category: FeatureCatalogueCategory.DATA, }); } + + return { + setDefaultEditor: (editor) => { + setDefaultEditor(editor); + }, + } as VisualizePluginSetup; } public start(core: CoreStart, plugins: VisualizePluginStartDependencies) { diff --git a/src/plugins/visualize/public/services.ts b/src/plugins/visualize/public/services.ts index 8190872ec6508..7994ad14543d5 100644 --- a/src/plugins/visualize/public/services.ts +++ b/src/plugins/visualize/public/services.ts @@ -21,6 +21,7 @@ import { ApplicationStart, IUiSettingsClient } from '../../../core/public'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; import { IndexPatternsContract, DataPublicPluginStart } from '../../../plugins/data/public'; import { SharePluginStart } from '../../../plugins/share/public'; +import { VisEditorConstructor } from './application/types'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -32,6 +33,10 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( + 'DefaultEditor' +); + export const [getQueryService, setQueryService] = createGetterSetter< DataPublicPluginStart['query'] >('Query'); diff --git a/tasks/check_plugins.js b/tasks/check_plugins.js deleted file mode 100644 index 20fb8a895af6c..0000000000000 --- a/tasks/check_plugins.js +++ /dev/null @@ -1,58 +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 fs from 'fs'; -import path from 'path'; - -export default function checkPlugins(grunt) { - grunt.registerTask( - 'checkPlugins', - 'Checks for plugins which may disrupt tests', - function checkPlugins() { - const done = this.async(); - const pluginsDir = path.resolve('./plugins/'); - - fs.readdir(pluginsDir, (err, files) => { - if (!files) { - return done(); - } - - const plugins = files.filter((file) => { - return fs.statSync(path.join(pluginsDir, file)).isDirectory(); - }); - - if (plugins.length) { - grunt.log.error( - '===================================================================================================' - ); - plugins.forEach((plugin) => { - grunt.log.error( - `The ${plugin} plugin may disrupt the test process. Consider removing it and re-running your tests.` - ); - }); - grunt.log.error( - '===================================================================================================' - ); - } - - done(); - }); - } - ); -} diff --git a/tasks/docker_docs.js b/tasks/docker_docs.js deleted file mode 100644 index 3a2041abc9301..0000000000000 --- a/tasks/docker_docs.js +++ /dev/null @@ -1,53 +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 del from 'del'; -import { join } from 'path'; -import { execFileSync as exec } from 'child_process'; - -export default function (grunt) { - grunt.registerTask('docker:docs', 'Build docs from docker', function () { - const rootPath = grunt.config.get('root'); - const composePath = join(rootPath, 'tasks/docker_docs/docker-compose.yml'); - const htmlDocsDir = join(rootPath, 'html_docs'); - - const env = Object.assign(process.env, { - KIBANA_DOCS_CONTAINER_NAME: 'kibana_docs', - KIBANA_DOCS_CONTEXT: rootPath, - }); - const stdio = [0, 1, 2]; - const execOptions = { env, stdio }; - - exec('docker-compose', ['-f', composePath, 'up'], execOptions); - - const containerId = String( - exec('docker-compose', ['-f', composePath, 'ps', '-q', env.KIBANA_DOCS_CONTAINER_NAME], { - env, - }) - ).trim(); - - grunt.log.write('Clearing old docs ... '); - del.sync(htmlDocsDir); - grunt.log.writeln('done'); - - grunt.log.write('Copying new docs ... '); - exec('docker', ['cp', `${containerId}:/home/kibana/html_docs`, htmlDocsDir]); - grunt.log.writeln('done'); - }); -} diff --git a/tasks/docker_docs/Dockerfile b/tasks/docker_docs/Dockerfile deleted file mode 100644 index 435b78f89f2e7..0000000000000 --- a/tasks/docker_docs/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM alpine:3.6 - -RUN apk add --no-cache \ - git \ - libxslt \ - curl \ - python \ - libxml2-utils \ - perl - -RUN addgroup kibana && \ - adduser -D -G kibana -s /bin/bash -h /home/kibana kibana - -USER kibana -RUN git clone --depth 1 https://github.com/elastic/docs.git /home/kibana/docs_builder - -WORKDIR /home/kibana/docs_builder -CMD git pull origin master && ./build_docs.pl --doc /home/kibana/ascii_docs/index.asciidoc --out /home/kibana/html_docs --chunk=1 diff --git a/tasks/docker_docs/docker-compose.yml b/tasks/docker_docs/docker-compose.yml deleted file mode 100644 index 8d0bd2c136596..0000000000000 --- a/tasks/docker_docs/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '2' -services: - kibana_docs: - container_name: $KIBANA_DOCS_CONTAINER_NAME - build: - context: $KIBANA_DOCS_CONTEXT/tasks/docker_docs - volumes: - - $KIBANA_DOCS_CONTEXT/docs:/home/kibana/ascii_docs diff --git a/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts index 861987c30705c..ac12e7ef04a1a 100644 --- a/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts +++ b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts @@ -22,8 +22,32 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('main', () => { + const basicIndex = 'ba*ic_index'; + let indexPattern: any; + + before(async () => { + await esArchiver.load('index_patterns/basic_index'); + + indexPattern = ( + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: basicIndex, + }, + }) + ).body.index_pattern; + }); + + after(async () => { + await esArchiver.unload('index_patterns/basic_index'); + + if (indexPattern) { + await supertest.delete('/api/index_patterns/index_pattern/' + indexPattern.id); + } + }); + it('can update multiple fields', async () => { const title = `foo-${Date.now()}-${Math.random()}*`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ @@ -171,48 +195,6 @@ export default function ({ getService }: FtrProviderContext) { expect(response3.status).to.be(200); expect(response3.body.index_pattern.fieldAttrs.foo.count).to.be(undefined); }); - - it('can set field "count" attribute on an existing field', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fields: { - foo: { - name: 'foo', - type: 'string', - count: 123, - }, - }, - }, - }); - - expect(response1.status).to.be(200); - expect(response1.body.index_pattern.fieldAttrs.foo).to.be(undefined); - expect(response1.body.index_pattern.fields.foo.count).to.be(123); - - const response2 = await supertest - .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) - .send({ - fields: { - foo: { - count: 456, - }, - }, - }); - - expect(response2.status).to.be(200); - expect(response2.body.index_pattern.fieldAttrs.foo).to.be(undefined); - expect(response2.body.index_pattern.fields.foo.count).to.be(456); - - const response3 = await supertest.get( - `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` - ); - - expect(response3.status).to.be(200); - expect(response3.body.index_pattern.fieldAttrs.foo).to.be(undefined); - expect(response3.body.index_pattern.fields.foo.count).to.be(456); - }); }); describe('customLabel', () => { @@ -323,46 +305,20 @@ export default function ({ getService }: FtrProviderContext) { }); it('can set field "customLabel" attribute on an existing field', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fields: { - foo: { - name: 'foo', - type: 'string', - count: 123, - customLabel: 'foo', - }, + await supertest.post(`/api/index_patterns/index_pattern/${indexPattern.id}/fields`).send({ + fields: { + foo: { + customLabel: 'baz', }, }, }); - expect(response1.status).to.be(200); - expect(response1.body.index_pattern.fieldAttrs.foo).to.be(undefined); - expect(response1.body.index_pattern.fields.foo.customLabel).to.be('foo'); - - const response2 = await supertest - .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) - .send({ - fields: { - foo: { - customLabel: 'baz', - }, - }, - }); - - expect(response2.status).to.be(200); - expect(response2.body.index_pattern.fieldAttrs.foo).to.be(undefined); - expect(response2.body.index_pattern.fields.foo.customLabel).to.be('baz'); - - const response3 = await supertest.get( - `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + const response1 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}` ); - expect(response3.status).to.be(200); - expect(response3.body.index_pattern.fieldAttrs.foo).to.be(undefined); - expect(response3.body.index_pattern.fields.foo.customLabel).to.be('baz'); + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fields.foo.customLabel).to.be('baz'); }); }); @@ -463,31 +419,8 @@ export default function ({ getService }: FtrProviderContext) { }); it('can remove "format" attribute from index_pattern format map', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fieldFormats: { - foo: { - id: 'bar', - params: { - baz: 'qux', - }, - }, - }, - }, - }); - - expect(response1.status).to.be(200); - expect(response1.body.index_pattern.fieldFormats.foo).to.eql({ - id: 'bar', - params: { - baz: 'qux', - }, - }); - const response2 = await supertest - .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .post(`/api/index_patterns/index_pattern/${indexPattern.id}/fields`) .send({ fields: { foo: { @@ -500,7 +433,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response2.body.index_pattern.fieldFormats.foo).to.be(undefined); const response3 = await supertest.get( - `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + `/api/index_patterns/index_pattern/${indexPattern.id}` ); expect(response3.status).to.be(200); diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts index bfc4c23738aef..9c6cb54d16f44 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts @@ -262,44 +262,6 @@ export default function ({ getService }: FtrProviderContext) { expect(response3.body.index_pattern.typeMeta).to.eql({ foo: 'baz' }); }); - it('can update index_pattern fields', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fields: { - foo: { - name: 'foo', - type: 'string', - }, - }, - }, - }); - - expect(response1.body.index_pattern.fields.foo.name).to.be('foo'); - expect(response1.body.index_pattern.fields.foo.type).to.be('string'); - - const id = response1.body.index_pattern.id; - const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ - index_pattern: { - fields: { - bar: { - name: 'bar', - type: 'number', - }, - }, - }, - }); - - expect(response2.body.index_pattern.fields.bar.name).to.be('bar'); - expect(response2.body.index_pattern.fields.bar.type).to.be('number'); - - const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); - - expect(response3.body.index_pattern.fields.bar.name).to.be('bar'); - expect(response3.body.index_pattern.fields.bar.type).to.be('number'); - }); - it('can update multiple index pattern fields at once', async () => { const title = `foo-${Date.now()}-${Math.random()}*`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/errors.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/errors.ts index 2182f47d91c08..7e26541680a39 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/errors.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/errors.ts @@ -21,9 +21,33 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); describe('errors', () => { + const basicIndex = 'b*sic_index'; + let indexPattern: any; + + before(async () => { + await esArchiver.load('index_patterns/basic_index'); + + indexPattern = ( + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: basicIndex, + }, + }) + ).body.index_pattern; + }); + + after(async () => { + await esArchiver.unload('index_patterns/basic_index'); + + if (indexPattern) { + await supertest.delete('/api/index_patterns/index_pattern/' + indexPattern.id); + } + }); + it('returns 404 error on non-existing index_pattern', async () => { const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; const response = await supertest.delete( @@ -34,35 +58,16 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns 404 error on non-existing scripted field', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - }, - }); - const response2 = await supertest.delete( - `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + const response1 = await supertest.delete( + `/api/index_patterns/index_pattern/${indexPattern.id}/scripted_field/test` ); - expect(response2.status).to.be(404); + expect(response1.status).to.be(404); }); it('returns error when attempting to delete a field which is not a scripted field', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fields: { - foo: { - scripted: false, - name: 'foo', - type: 'string', - }, - }, - }, - }); const response2 = await supertest.delete( - `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + `/api/index_patterns/index_pattern/${indexPattern.id}/scripted_field/foo` ); expect(response2.status).to.be(400); diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/errors.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/errors.ts index 1f39de8c03a96..71c28507b209d 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/errors.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/errors.ts @@ -21,9 +21,33 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); describe('errors', () => { + const basicIndex = '*asic_index'; + let indexPattern: any; + + before(async () => { + await esArchiver.load('index_patterns/basic_index'); + + indexPattern = ( + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: basicIndex, + }, + }) + ).body.index_pattern; + }); + + after(async () => { + await esArchiver.unload('index_patterns/basic_index'); + + if (indexPattern) { + await supertest.delete('/api/index_patterns/index_pattern/' + indexPattern.id); + } + }); + it('returns 404 error on non-existing index_pattern', async () => { const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; const response = await supertest.get( @@ -34,40 +58,11 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns 404 error on non-existing scripted field', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - }, - }); - const response2 = await supertest.get( - `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` - ); - - expect(response2.status).to.be(404); - }); - - it('returns error when attempting to fetch a field which is not a scripted field', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fields: { - foo: { - scripted: false, - name: 'foo', - type: 'string', - }, - }, - }, - }); - const response2 = await supertest.get( - `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + const response1 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}/scripted_field/sf` ); - expect(response2.status).to.be(400); - expect(response2.body.statusCode).to.be(400); - expect(response2.body.message).to.be('Only scripted fields can be retrieved.'); + expect(response1.status).to.be(404); }); it('returns error when ID is too long', async () => { @@ -81,5 +76,22 @@ export default function ({ getService }: FtrProviderContext) { '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' ); }); + + it('returns 404 error on non-existing scripted field', async () => { + const response1 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}/scripted_field/test` + ); + expect(response1.status).to.be(404); + }); + + it('returns error when attempting to fetch a field which is not a scripted field', async () => { + const response2 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}/scripted_field/foo` + ); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only scripted fields can be retrieved.'); + }); }); } diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index 7254f3b3fcf31..43bf37275c00f 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -167,7 +167,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: 'Bad Request', + message: 'Error fetching objects to export', attributes: { objects: [ { diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts index bdb695ef20dd1..b661822f4dcd3 100644 --- a/test/api_integration/apis/saved_objects/import.ts +++ b/test/api_integration/apis/saved_objects/import.ts @@ -20,12 +20,12 @@ import expect from '@kbn/expect'; import { join } from 'path'; import dedent from 'dedent'; -import type { SavedObjectsImportError } from 'src/core/server'; +import type { SavedObjectsImportFailure } from 'src/core/server'; import type { FtrProviderContext } from '../../ftr_provider_context'; const createConflictError = ( - object: Omit -): SavedObjectsImportError => ({ + object: Omit +): SavedObjectsImportFailure => ({ ...object, title: object.meta.title, error: { type: 'conflict' }, diff --git a/test/functional/apps/dashboard/embeddable_data_grid.ts b/test/functional/apps/dashboard/embeddable_data_grid.ts new file mode 100644 index 0000000000000..067536ab7aa93 --- /dev/null +++ b/test/functional/apps/dashboard/embeddable_data_grid.ts @@ -0,0 +1,60 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dashboardAddPanel = getService('dashboardAddPanel'); + const filterBar = getService('filterBar'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const find = getService('find'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']); + + describe('dashboard embeddable data grid', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('dashboard/current/data'); + await esArchiver.loadIfNeeded('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + 'doc_table:legacy': false, + }); + await PageObjects.common.navigateToApp('dashboard'); + await filterBar.ensureFieldEditorModalIsClosed(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.timePicker.setDefaultDataRange(); + }); + + describe('saved search filters', function () { + it('are added when a cell filter is clicked', async function () { + await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); + await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`); + await find.clickByCssSelector(`[data-test-subj="filterOutButton"]`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`); + await find.clickByCssSelector(`[data-test-subj="filterForButton"]`); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(2); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 6fb5f874022a0..43ad1aad5de00 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -54,6 +54,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./empty_dashboard')); loadTestFile(require.resolve('./url_field_formatter')); loadTestFile(require.resolve('./embeddable_rendering')); + loadTestFile(require.resolve('./embeddable_data_grid')); loadTestFile(require.resolve('./create_and_add_embeddables')); loadTestFile(require.resolve('./edit_embeddable_redirects')); loadTestFile(require.resolve('./edit_visualizations')); diff --git a/test/functional/apps/dashboard/legacy_urls.ts b/test/functional/apps/dashboard/legacy_urls.ts index 6bb8d808e8daa..2a30bbf5e1f0a 100644 --- a/test/functional/apps/dashboard/legacy_urls.ts +++ b/test/functional/apps/dashboard/legacy_urls.ts @@ -92,6 +92,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(`[abc](#/dashboard/${testDashboardId})`); await PageObjects.visEditor.clickGo(); + + await PageObjects.visualize.saveVisualizationExpectSuccess('legacy url markdown'); + (await find.byLinkText('abc')).click(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -109,6 +112,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.expectMarkdownTextArea(); await browser.goForward(); }); + + it('resolves markdown link from dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.addVisualization('legacy url markdown'); + (await find.byLinkText('abc')).click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.setDefaultDataRange(); + + await PageObjects.dashboard.waitForRenderComplete(); + await pieChart.expectPieSliceCount(5); + }); }); }); } diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts new file mode 100644 index 0000000000000..8f62e03518253 --- /dev/null +++ b/test/functional/apps/discover/_data_grid.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +export default function ({ + getService, + getPageObjects, +}: { + getService: (service: string) => any; + getPageObjects: (pageObjects: string[]) => any; +}) { + describe('discover data grid tests', function describeDiscoverDataGrid() { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const kibanaServer = getService('kibanaServer'); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const testSubjects = getService('testSubjects'); + + before(async function () { + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + after(async function () { + await kibanaServer.uiSettings.replace({ 'doc_table:legacy': true }); + }); + + it('can add fields to the table', async function () { + const getTitles = async () => + (await testSubjects.getVisibleText('dataGridHeader')).replace(/\s|\r?\n|\r/g, ' '); + + expect(await getTitles()).to.be('Time (@timestamp) _source'); + + await PageObjects.discover.clickFieldListItemAdd('bytes'); + expect(await getTitles()).to.be('Time (@timestamp) bytes'); + + await PageObjects.discover.clickFieldListItemAdd('agent'); + expect(await getTitles()).to.be('Time (@timestamp) bytes agent'); + + await PageObjects.discover.clickFieldListItemAdd('bytes'); + expect(await getTitles()).to.be('Time (@timestamp) agent'); + + await PageObjects.discover.clickFieldListItemAdd('agent'); + expect(await getTitles()).to.be('Time (@timestamp) _source'); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts new file mode 100644 index 0000000000000..6821b9c69cf7e --- /dev/null +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -0,0 +1,91 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const TEST_COLUMN_NAMES = ['@message']; +const TEST_FILTER_COLUMN_NAMES = [ + ['extension', 'jpg'], + ['geo.src', 'IN'], +]; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const filterBar = getService('filterBar'); + const dataGrid = getService('dataGrid'); + const docTable = getService('docTable'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + describe('discover data grid context tests', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + + for (const columnName of TEST_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItemAdd(columnName); + } + + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + } + }); + after(async () => { + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + }); + + it('should open the context view with the selected document as anchor', async () => { + // check the anchor timestamp in the context view + await retry.waitFor('selected document timestamp matches anchor timestamp ', async () => { + // get the timestamp of the first row + const discoverFields = await dataGrid.getFields(); + const firstTimestamp = discoverFields[0][0]; + + // navigate to the context view + await dataGrid.clickRowToggle({ rowIndex: 0 }); + const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); + await rowActions[1].click(); + // entering the context view (contains the legacy type) + const contextFields = await docTable.getFields(); + const anchorTimestamp = contextFields[0][0]; + return anchorTimestamp === firstTimestamp; + }); + }); + + it('should open the context view with the same columns', async () => { + const columnNames = await docTable.getHeaderFields(); + expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]); + }); + + it('should open the context view with the filters disabled', async () => { + let disabledFilterCounter = 0; + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + if (await filterBar.hasFilter(columnName, value, false)) { + disabledFilterCounter++; + } + } + expect(disabledFilterCounter).to.be(TEST_FILTER_COLUMN_NAMES.length); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts new file mode 100644 index 0000000000000..92d9893cab0b6 --- /dev/null +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -0,0 +1,91 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const filterBar = getService('filterBar'); + const dataGrid = getService('dataGrid'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const kibanaServer = getService('kibanaServer'); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + + describe('discover data grid doc link', function () { + beforeEach(async function () { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + }); + + it('should open the doc view of the selected document', async function () { + // navigate to the doc view + await dataGrid.clickRowToggle({ rowIndex: 0 }); + + // click the open action + await retry.try(async () => { + const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); + if (!rowActions.length) { + throw new Error('row actions empty, trying again'); + } + await rowActions[0].click(); + }); + + const hasDocHit = await testSubjects.exists('doc-hit'); + expect(hasDocHit).to.be(true); + }); + + it('add filter should create an exists filter if value is null (#7189)', async function () { + await PageObjects.discover.waitUntilSearchingHasFinished(); + // Filter special document + await filterBar.addFilter('agent', 'is', 'Missing/Fields'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await retry.try(async () => { + // navigate to the doc view + await dataGrid.clickRowToggle({ rowIndex: 0 }); + + const details = await dataGrid.getDetailsRow(); + await dataGrid.addInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const hasInclusiveFilter = await filterBar.hasFilter( + 'referer', + 'exists', + true, + false, + true + ); + expect(hasInclusiveFilter).to.be(true); + + await dataGrid.clickRowToggle({ rowIndex: 0 }); + const detailsExcluding = await dataGrid.getDetailsRow(); + await dataGrid.removeInclusiveFilter(detailsExcluding, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false); + expect(hasExcludeFilter).to.be(true); + }); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts new file mode 100644 index 0000000000000..1224823abf048 --- /dev/null +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -0,0 +1,132 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dataGrid = getService('dataGrid'); + const log = getService('log'); + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultSettings = { + defaultIndex: 'logstash-*', + 'doc_table:legacy': false, + }; + + describe('discover data grid doc table', function describeIndexTests() { + const defaultRowsLimit = 25; + + before(async function () { + log.debug('load kibana index with default index pattern'); + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + + it('should show the first 50 rows by default', async function () { + // with the default range the number of hits is ~14000 + const rows = await dataGrid.getDocTableRows(); + expect(rows.length).to.be(defaultRowsLimit); + }); + + it('should refresh the table content when changing time window', async function () { + const initialRows = await dataGrid.getDocTableRows(); + + const fromTime = 'Sep 20, 2015 @ 23:00:00.000'; + const toTime = 'Sep 20, 2015 @ 23:14:00.000'; + + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const finalRows = await PageObjects.discover.getDocTableRows(); + expect(finalRows.length).to.be.below(initialRows.length); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + describe('expand a document row', function () { + const rowToInspect = 1; + + it('should expand the detail row when the toggle arrow is clicked', async function () { + await retry.try(async function () { + await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 }); + const detailsEl = await dataGrid.getDetailsRows(); + const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle'); + expect(defaultMessageEl).to.be.ok(); + await dataGrid.closeFlyout(); + }); + }); + + it('should show the detail panel actions', async function () { + await retry.try(async function () { + await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 }); + const [surroundingActionEl, singleActionEl] = await dataGrid.getRowActions({ + isAnchorRow: false, + rowIndex: rowToInspect - 1, + }); + expect(surroundingActionEl).to.be.ok(); + expect(singleActionEl).to.be.ok(); + await dataGrid.closeFlyout(); + }); + }); + }); + + describe('add and remove columns', function () { + const extraColumns = ['phpmemory', 'ip']; + + afterEach(async function () { + for (const column of extraColumns) { + await PageObjects.discover.clickFieldListItemRemove(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + }); + + it('should add more columns to the table', async function () { + for (const column of extraColumns) { + await PageObjects.discover.clearFieldSearchInput(); + await PageObjects.discover.findFieldByName(column); + await PageObjects.discover.clickFieldListItemAdd(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + // test the header now + const header = await dataGrid.getHeaderFields(); + expect(header.join(' ')).to.have.string(column); + } + }); + + it('should remove columns from the table', async function () { + for (const column of extraColumns) { + await PageObjects.discover.clearFieldSearchInput(); + await PageObjects.discover.findFieldByName(column); + await PageObjects.discover.clickFieldListItemAdd(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + // remove the second column + await PageObjects.discover.clickFieldListItemAdd(extraColumns[1]); + await PageObjects.header.waitUntilLoadingHasFinished(); + // test that the second column is no longer there + const header = await dataGrid.getHeaderFields(); + expect(header.join(' ')).to.not.have.string(extraColumns[1]); + }); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts new file mode 100644 index 0000000000000..8224f59f7fabf --- /dev/null +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -0,0 +1,99 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); + const queryBar = getService('queryBar'); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const dataGrid = getService('dataGrid'); + + describe('discover data grid field data tests', function describeIndexTests() { + this.tags('includeFirefox'); + before(async function () { + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + }); + describe('field data', function () { + it('search php should show the correct hit count', async function () { + const expectedHitCount = '445'; + await retry.try(async function () { + await queryBar.setQuery('php'); + await queryBar.submitQuery(); + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.be(expectedHitCount); + }); + }); + + it('the search term should be highlighted in the field data', async function () { + // marks is the style that highlights the text in yellow + const marks = await PageObjects.discover.getMarks(); + expect(marks.length).to.be(25); + expect(marks.indexOf('php')).to.be(0); + }); + + it('search type:apache should show the correct hit count', async function () { + const expectedHitCount = '11,156'; + await queryBar.setQuery('type:apache'); + await queryBar.submitQuery(); + await retry.try(async function tryingForTime() { + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.be(expectedHitCount); + }); + }); + + it('doc view should show Time and _source columns', async function () { + const expectedHeader = 'Time (@timestamp) _source'; + const DocHeader = await dataGrid.getHeaderFields(); + expect(DocHeader.join(' ')).to.be(expectedHeader); + }); + + it('doc view should sort ascending', async function () { + const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000'; + await dataGrid.clickDocSortAsc(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await retry.try(async function tryingForTime() { + const rowData = await dataGrid.getFields(); + expect(rowData[0][0].startsWith(expectedTimeStamp)).to.be.ok(); + }); + }); + + it('a bad syntax query should show an error message', async function () { + const expectedError = + 'Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' + + 'whitespace but "(" found.'; + await queryBar.setQuery('xxx(yyy))'); + await queryBar.submitQuery(); + const { message } = await toasts.getErrorToast(); + expect(message).to.contain(expectedError); + await toasts.dismissToast(); + }); + }); + }); +} diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index e52c33078029a..1b333c377f777 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -110,7 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should modify the time range when the histogram is brushed', async function () { + it.skip('should modify the time range when the histogram is brushed', async function () { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.brushHistogram(); await PageObjects.discover.waitUntilSearchingHasFinished(); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 20fda144b338e..40a6ab31f7d4c 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -131,13 +131,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should add more columns to the table', async function () { - const [column] = extraColumns; - await PageObjects.discover.findFieldByName(column); - log.debug(`add a ${column} column`); - await PageObjects.discover.clickFieldListItemAdd(column); - await PageObjects.header.waitUntilLoadingHasFinished(); - // test the header now - expect(await PageObjects.discover.getDocHeader()).to.have.string(column); + for (const column of extraColumns) { + await PageObjects.discover.clearFieldSearchInput(); + await PageObjects.discover.findFieldByName(column); + await PageObjects.discover.clickFieldListItemAdd(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + // test the header now + expect(await PageObjects.discover.getDocHeader()).to.have.string(column); + } }); it('should remove columns from the table', async function () { diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index c13529b7d1b43..450049af66abf 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -51,5 +51,10 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_date_nanos')); loadTestFile(require.resolve('./_date_nanos_mixed')); loadTestFile(require.resolve('./_indexpattern_without_timefield')); + loadTestFile(require.resolve('./_data_grid')); + loadTestFile(require.resolve('./_data_grid_context')); + loadTestFile(require.resolve('./_data_grid_field_data')); + loadTestFile(require.resolve('./_data_grid_doc_navigation')); + loadTestFile(require.resolve('./_data_grid_doc_table')); }); } diff --git a/test/functional/apps/timelion/_expression_typeahead.js b/test/functional/apps/timelion/_expression_typeahead.js index 5d834f1a055de..adad6f1c57acd 100644 --- a/test/functional/apps/timelion/_expression_typeahead.js +++ b/test/functional/apps/timelion/_expression_typeahead.js @@ -88,14 +88,14 @@ export default function ({ getPageObjects }) { const suggestions = await PageObjects.timelion.getSuggestionItemsText(); expect(suggestions.length).to.eql(51); expect(suggestions[0].includes('@message.raw')).to.eql(true); - await PageObjects.timelion.clickSuggestion(10); + await PageObjects.timelion.clickSuggestion(10, 2000); }); it('should show field suggestions for metric argument when index pattern set', async () => { await PageObjects.timelion.updateExpression(',metric'); await PageObjects.timelion.clickSuggestion(); await PageObjects.timelion.updateExpression('avg:'); - await PageObjects.timelion.clickSuggestion(); + await PageObjects.timelion.clickSuggestion(0, 2000); const suggestions = await PageObjects.timelion.getSuggestionItemsText(); expect(suggestions.length).to.eql(2); expect(suggestions[0].includes('avg:bytes')).to.eql(true); diff --git a/test/functional/config.js b/test/functional/config.js index 5bef9896d17cc..ea6e75b174b4c 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -59,6 +59,7 @@ export default async function ({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', + 'visualization:visualize:legacyChartsLibrary': true, }, }, diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 8924d22cdb50f..cc1420e4825c2 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -33,6 +33,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide const dashboardAddPanel = getService('dashboardAddPanel'); const renderable = getService('renderable'); const listingTable = getService('listingTable'); + const elasticChart = getService('elasticChart'); const PageObjects = getPageObjects(['common', 'header', 'visualize']); interface SaveDashboardOptions { @@ -275,6 +276,20 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide } } + public async isColorSyncOn() { + log.debug('isColorSyncOn'); + await this.openOptions(); + return await testSubjects.getAttribute('dashboardSyncColorsCheckbox', 'checked'); + } + + public async useColorSync(on = true) { + await this.openOptions(); + const isColorSyncOn = await this.isColorSyncOn(); + if (isColorSyncOn !== 'on') { + return await testSubjects.click('dashboardSyncColorsCheckbox'); + } + } + public async gotoDashboardEditMode(dashboardName: string) { await this.loadSavedDashboard(dashboardName); await this.switchToEditMode(); @@ -554,6 +569,10 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide return 0; } } + + public async getPanelChartDebugState(panelIndex: number) { + return await elasticChart.getChartDebugData(undefined, panelIndex); + } } return new DashboardPage(); diff --git a/test/functional/page_objects/header_page.ts b/test/functional/page_objects/header_page.ts index 5a892bb4f6ca3..68d3c27d437e4 100644 --- a/test/functional/page_objects/header_page.ts +++ b/test/functional/page_objects/header_page.ts @@ -25,7 +25,6 @@ export function HeaderPageProvider({ getService, getPageObjects }: FtrProviderCo const retry = getService('retry'); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); - const globalNav = getService('globalNav'); const PageObjects = getPageObjects(['common']); const defaultFindTimeout = config.get('timeouts.find'); @@ -42,14 +41,9 @@ export function HeaderPageProvider({ getService, getPageObjects }: FtrProviderCo await appsMenu.clickLink('Visualize', { category: 'kibana' }); await this.onAppLeaveWarning(ignoreAppLeaveWarning); await this.awaitGlobalLoadingIndicatorHidden(); - await retry.waitFor('first breadcrumb to be "Visualize"', async () => { - const firstBreadcrumb = await globalNav.getFirstBreadcrumb(); - if (firstBreadcrumb !== 'Visualize') { - log.debug('-- first breadcrumb =', firstBreadcrumb); - return false; - } - - return true; + await retry.waitFor('Visualize app to be loaded', async () => { + const isNavVisible = await testSubjects.exists('top-nav'); + return isNavVisible; }); } diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index 22e1769145f88..ff1e934c7f265 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -149,8 +149,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F await PageObjects.visualize.clickAggBasedVisualizations(); await PageObjects.visualize.clickMetric(); await find.clickByCssSelector('li.euiListGroupItem:nth-of-type(2)'); - await testSubjects.exists('visualizeSaveButton'); - await testSubjects.click('visualizeSaveButton'); + await testSubjects.exists('visualizesaveAndReturnButton'); + await testSubjects.click('visualizesaveAndReturnButton'); } async createAndEmbedMarkdown({ name, markdown }: { name: string; markdown: string }) { @@ -163,7 +163,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(markdown); await PageObjects.visEditor.clickGo(); - await testSubjects.click('visualizeSaveButton'); + await testSubjects.click('visualizesaveAndReturnButton'); } })(); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 209e30d23ca3c..c538d8156103c 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -24,10 +24,15 @@ interface TabbedGridData { columns: string[]; rows: string[][]; } +interface SelectOptions { + isAnchorRow?: boolean; + rowIndex: number; +} -export function DataGridProvider({ getService }: FtrProviderContext) { +export function DataGridProvider({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); class DataGrid { async getDataGridTableData(): Promise { @@ -103,6 +108,137 @@ export function DataGridProvider({ getService }: FtrProviderContext) { [data-test-subj="dataGridRowCell"]:nth-of-type(${columnIndex})` ); } + public async getFields() { + const rows = await find.allByCssSelector('.euiDataGridRow'); + + const result = []; + for (const row of rows) { + const cells = await row.findAllByClassName('euiDataGridRowCell__truncate'); + const cellsText = []; + let cellIdx = 0; + for (const cell of cells) { + if (cellIdx > 0) { + cellsText.push(await cell.getVisibleText()); + } + cellIdx++; + } + result.push(cellsText); + } + return result; + } + + public async getTable(selector: string = 'docTable') { + return await testSubjects.find(selector); + } + + public async getBodyRows(): Promise { + const table = await this.getTable(); + return await table.findAllByTestSubject('dataGridRow'); + } + + public async getDocTableRows() { + const table = await this.getTable(); + return await table.findAllByTestSubject('dataGridRow'); + } + + public async getAnchorRow(): Promise { + const table = await this.getTable(); + return await table.findByTestSubject('~docTableAnchorRow'); + } + + public async getRow(options: SelectOptions): Promise { + return options.isAnchorRow + ? await this.getAnchorRow() + : (await this.getBodyRows())[options.rowIndex]; + } + + public async clickRowToggle( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const row = await this.getRow(options); + const toggle = await row.findByTestSubject('~docTableExpandToggleColumn'); + await toggle.click(); + } + + public async getDetailsRows(): Promise { + return await testSubjects.findAll('docTableDetailsFlyout'); + } + + public async closeFlyout() { + await testSubjects.click('euiFlyoutCloseButton'); + } + + public async getHeaderFields(): Promise { + const result = await find.allByCssSelector('.euiDataGridHeaderCell__content'); + const textArr = []; + let idx = 0; + for (const cell of result) { + if (idx > 0) { + textArr.push(await cell.getVisibleText()); + } + idx++; + } + return Promise.resolve(textArr); + } + + public async getRowActions( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const detailsRow = (await this.getDetailsRows())[options.rowIndex]; + return await detailsRow.findAllByTestSubject('~docTableRowAction'); + } + + public async clickDocSortAsc() { + await find.clickByCssSelector('.euiDataGridHeaderCell__button'); + await find.clickByButtonText('Sort New-Old'); + } + + public async clickDocSortDesc() { + await find.clickByCssSelector('.euiDataGridHeaderCell__button'); + await find.clickByButtonText('Sort Old-New'); + } + public async getDetailsRow(): Promise { + const detailRows = await this.getDetailsRows(); + return detailRows[0]; + } + public async addInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async getAddInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`); + } + + public async getTableDocViewRow( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`); + } + + public async getRemoveInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); + } + + public async removeInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } } return new DataGrid(); diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index de895918efbba..546f83e5b710a 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -17,6 +17,7 @@ * under the License. */ +import classNames from 'classnames'; import { FtrProviderContext } from '../ftr_provider_context'; export function FilterBarProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -45,7 +46,14 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon const filterPinnedState = pinned ? 'pinned' : 'unpinned'; const filterNegatedState = negated ? 'filter-negated' : ''; return testSubjects.exists( - `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState} ${filterNegatedState}`, + classNames( + 'filter', + `filter-${filterActivationState}`, + key !== '' && `filter-key-${key}`, + value !== '' && `filter-value-${value}`, + `filter-${filterPinnedState}`, + filterNegatedState + ), { allowHidden: true, } diff --git a/test/functional/services/visualizations/elastic_chart.ts b/test/functional/services/visualizations/elastic_chart.ts index 1f1f7df45f460..86ca4d1c1e31e 100644 --- a/test/functional/services/visualizations/elastic_chart.ts +++ b/test/functional/services/visualizations/elastic_chart.ts @@ -81,19 +81,23 @@ export function ElasticChartProvider({ getService }: FtrProviderContext) { } } - private async getChart(dataTestSubj?: string, timeout?: number): Promise { + private async getChart( + dataTestSubj?: string, + timeout?: number, + match: number = 0 + ): Promise { if (dataTestSubj) { if (!(await testSubjects.exists(dataTestSubj, { timeout }))) { throw Error(`Failed to find an elastic-chart with testSubject '${dataTestSubj}'`); } - return await testSubjects.find(dataTestSubj); + return (await testSubjects.findAll(dataTestSubj))[match]; } else { const charts = await this.getAllCharts(timeout); if (charts.length === 0) { throw Error(`Failed to find any elastic-charts on the page`); } else { - return charts[0]; + return charts[match]; } } } @@ -106,8 +110,11 @@ export function ElasticChartProvider({ getService }: FtrProviderContext) { * used to get chart data from `@elastic/charts` * requires `window._echDebugStateFlag` to be true */ - public async getChartDebugData(dataTestSubj?: string): Promise { - const chart = await this.getChart(dataTestSubj); + public async getChartDebugData( + dataTestSubj?: string, + match: number = 0 + ): Promise { + const chart = await this.getChart(dataTestSubj, undefined, match); try { const visContainer = await chart.findByCssSelector('.echChartStatus'); diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_action.tsx b/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_action.tsx index 18e45c22abaca..1d79fcae3c1a3 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_action.tsx +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_action.tsx @@ -21,19 +21,18 @@ import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import React from 'react'; import { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; -import { createAction, ActionType } from '../../../../../src/plugins/ui_actions/public'; +import { createAction } from '../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -// Casting to ActionType is a hack - in a real situation use -// declare module and add this id to ActionContextMapping. -export const SAMPLE_PANEL_ACTION = 'samplePanelAction' as ActionType; +export const SAMPLE_PANEL_ACTION = 'samplePanelAction'; export interface SamplePanelActionContext { embeddable: IEmbeddable; } export function createSamplePanelAction(getStartServices: CoreSetup['getStartServices']) { - return createAction({ + return createAction({ + id: SAMPLE_PANEL_ACTION, type: SAMPLE_PANEL_ACTION, getDisplayName: () => 'Sample Panel Action', execute: async ({ embeddable }: SamplePanelActionContext) => { 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 faa774b8485b1..ec6a2286ca33f 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 @@ -16,14 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { Action, createAction, ActionType } from '../../../../../src/plugins/ui_actions/public'; +import { Action, createAction } from '../../../../../src/plugins/ui_actions/public'; -// Casting to ActionType is a hack - in a real situation use -// declare module and add this id to ActionContextMapping. -export const SAMPLE_PANEL_LINK = 'samplePanelLink' as ActionType; +export const SAMPLE_PANEL_LINK = 'samplePanelLink'; export const createSamplePanelLink = (): Action => - createAction({ + createAction({ + id: SAMPLE_PANEL_LINK, type: SAMPLE_PANEL_LINK, getDisplayName: () => 'Sample panel Link', execute: async () => { diff --git a/test/scripts/checks/mocha_coverage.sh b/test/scripts/checks/mocha_coverage.sh deleted file mode 100755 index e1afad0ab775f..0000000000000 --- a/test/scripts/checks/mocha_coverage.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn nyc --reporter=html --reporter=json-summary --report-dir=./target/kibana-coverage/mocha node scripts/mocha diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index f449986713f97..6184708ea3fc6 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -10,7 +10,7 @@ fi export KBN_NP_PLUGINS_BUILT=true echo " -> Ensuring all functional tests are in a ciGroup" -yarn run grunt functionalTests:ensureAllTestsInCiGroup; +node scripts/ensure_all_tests_in_ci_group; # Do not build kibana for code coverage run if [[ -z "$CODE_COVERAGE" ]] ; then diff --git a/test/scripts/jenkins_docs.sh b/test/scripts/jenkins_docs.sh deleted file mode 100755 index f447afda1f948..0000000000000 --- a/test/scripts/jenkins_docs.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -e -source "$(dirname $0)/../../src/dev/ci_setup/setup.sh" - -"$(FORCE_COLOR=0 yarn bin)/grunt" docker:docs; diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 7384cec36b277..2edd66579f72f 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -44,8 +44,4 @@ else rename_coverage_file "oss-integration" echo "" echo "" - echo " -> Running mocha tests with coverage" - ./test/scripts/checks/mocha_coverage.sh - echo "" - echo "" fi diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh index e5f3259926e42..5e005c89330ca 100755 --- a/test/scripts/test/mocha.sh +++ b/test/scripts/test/mocha.sh @@ -2,5 +2,6 @@ source src/dev/ci_setup/setup_env.sh -checks-reporter-with-killswitch "Mocha Tests" \ - node scripts/mocha +# TODO: will remove mocha in another PR +# checks-reporter-with-killswitch "Mocha Tests" \ +# node scripts/mocha diff --git a/test/scripts/test/xpack_karma.sh b/test/scripts/test/xpack_karma.sh deleted file mode 100755 index 9078f01f1b870..0000000000000 --- a/test/scripts/test/xpack_karma.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -cd x-pack -checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma diff --git a/test/tsconfig.json b/test/tsconfig.json index df26441b0806f..f9008505ed66e 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -8,13 +8,22 @@ "exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], "references": [ { "path": "../src/core/tsconfig.json" }, + { "path": "../src/plugins/management/tsconfig.json" }, + { "path": "../src/plugins/bfetch/tsconfig.json" }, + { "path": "../src/plugins/embeddable/tsconfig.json" }, + { "path": "../src/plugins/expressions/tsconfig.json" }, + { "path": "../src/plugins/home/tsconfig.json" }, { "path": "../src/plugins/inspector/tsconfig.json" }, { "path": "../src/plugins/kibana_react/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../src/plugins/navigation/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, - { "path": "../src/plugins/usage_collection/tsconfig.json" } + { "path": "../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../src/plugins/usage_collection/tsconfig.json" }, ] } diff --git a/tsconfig.json b/tsconfig.json index 02048414f678e..20e2e57ce654e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,19 +7,30 @@ "exclude": [ "src/**/__fixtures__/**/*", "src/core/**/*", + "src/plugins/management/**/*", + "src/plugins/apm_oss/**/*", + "src/plugins/bfetch/**/*", + "src/plugins/data/**/*", "src/plugins/dev_tools/**/*", + "src/plugins/embeddable/**/*", + "src/plugins/expressions/**/*", + "src/plugins/home/**/*", "src/plugins/inspector/**/*", "src/plugins/kibana_legacy/**/*", "src/plugins/kibana_react/**/*", "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", + "src/plugins/navigation/**/*", "src/plugins/newsfeed/**/*", + "src/plugins/saved_objects/**/*", "src/plugins/security_oss/**/*", + "src/plugins/saved_objects_tagging_oss/**/*", "src/plugins/share/**/*", "src/plugins/telemetry/**/*", "src/plugins/telemetry_collection_manager/**/*", + "src/plugins/ui_actions/**/*", "src/plugins/url_forwarding/**/*", - "src/plugins/usage_collection/**/*", + "src/plugins/usage_collection/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -28,18 +39,29 @@ ], "references": [ { "path": "./src/core/tsconfig.json" }, + { "path": "./src/plugins/management/tsconfig.json"}, + { "path": "./src/plugins/apm_oss/tsconfig.json" }, + { "path": "./src/plugins/bfetch/tsconfig.json" }, + { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, + { "path": "./src/plugins/embeddable/tsconfig.json" }, + { "path": "./src/plugins/expressions/tsconfig.json" }, + { "path": "./src/plugins/home/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/saved_objects/tsconfig.json" }, + { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/plugins/usage_collection/tsconfig.json" } ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index a99d4d57d3f0a..c27d2ff2ec6f0 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -2,18 +2,29 @@ "include": [], "references": [ { "path": "./src/core/tsconfig.json" }, + { "path": "./src/plugins/apm_oss/tsconfig.json" }, + { "path": "./src/plugins/bfetch/tsconfig.json" }, + { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, + { "path": "./src/plugins/embeddable/tsconfig.json" }, + { "path": "./src/plugins/expressions/tsconfig.json" }, + { "path": "./src/plugins/home/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/saved_objects/tsconfig.json" }, + { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/plugins/management/tsconfig.json" }, ] } diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 521672e4bf48c..019eb8088dbfc 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -60,7 +60,6 @@ def uploadCoverageHtmls(prefix) { [ 'target/kibana-coverage/functional-combined', 'target/kibana-coverage/jest-combined', - 'target/kibana-coverage/mocha-combined', ].each { uploadWithVault(prefix, it) } } @@ -78,7 +77,6 @@ def prokLinks(title) { kibanaPipeline.bash(''' cat << EOF > src/dev/code_coverage/www/index_partial_2.html Latest Jest - Latest Mocha Latest FTR
@@ -151,7 +149,6 @@ def generateReports(title) { . src/dev/code_coverage/shell_scripts/extract_archives.sh . src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh . src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh - . src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh # zip combined reports tar -czf kibana-coverage.tar.gz target/kibana-coverage/**/* """, title) diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 221e93fd7b839..18be6e69a2637 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -115,14 +115,16 @@ def functionalXpack(Map params = [:]) { task(kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh')) } - whenChanged([ - 'x-pack/plugins/security_solution/', - 'x-pack/test/security_solution_cypress/', - 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', - 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx', - ]) { - task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) - } + whenChanged([ + 'x-pack/plugins/security_solution/', + 'x-pack/test/security_solution_cypress/', + 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', + 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx', + ]) { + if (githubPr.isPr()) { + task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) + } + } } } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6937862d20536..7380d25930bc0 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -42,7 +42,7 @@ "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["plugins/rollup"], - "xpack.runtimeFields": "plugins/runtime_fields", + "xpack.runtimeFields": "plugins/runtime_field_editor", "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/README.md b/x-pack/README.md index 41ea4cc4e469a..3f651336f7f3b 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -84,8 +84,9 @@ Jest integration tests can be used to test behavior with Elasticsearch and the K yarn test:jest_integration ``` -An example test exists at [test_utils/jest/integration_tests/example_integration.test.ts](test_utils/jest/integration_tests/example_integration.test.ts) - #### Running Reporting functional tests -See [here](test/reporting/README.md) for more information on running reporting tests. +See [here](./test/functional/apps/dashboard/reporting/README.md) for more information on running reporting tests. + +#### Running Security Solution Cypress E2E/integration tests +See [here](./plugins/security_solution/cypress/README.md) for information on running this test suite. diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md index ce7e110a5f914..51c034e510024 100644 --- a/x-pack/build_chromium/README.md +++ b/x-pack/build_chromium/README.md @@ -1,13 +1,15 @@ # Chromium build -We ship our own headless build of Chromium which is significantly smaller than the standard binaries shipped by Google. The scripts in this folder can be used to initialize the build environments and run the build on Mac, Windows, and Linux. +We ship our own headless build of Chromium which is significantly smaller than +the standard binaries shipped by Google. The scripts in this folder can be used +to accept a commit hash from the Chromium repository, and initialize the build +environments and run the build on Mac, Windows, and Linux. -The official Chromium build process is poorly documented, and seems to have breaking changes fairly regularly. The build pre-requisites, and the build flags change over time, so it is likely that the scripts in this directory will be out of date by the time we have to do another Chromium build. - -This document is an attempt to note all of the gotchas we've come across while building, so that the next time we have to tinker here, we'll have a good starting point. - -# Before you begin -You'll need access to our GCP account, which is where we have two machines provisioned for the Linux and Windows builds. Mac builds can be achieved locally, and are a great place to start to gain familiarity. +## Before you begin +If you wish to use a remote VM to build, you'll need access to our GCP account, +which is where we have two machines provisioned for the Linux and Windows +builds. Mac builds can be achieved locally, and are a great place to start to +gain familiarity. 1. Login to our GCP instance [here using your okta credentials](https://console.cloud.google.com/). 2. Click the "Compute Engine" tab. @@ -15,21 +17,89 @@ You'll need access to our GCP account, which is where we have two machines provi 4. If #3 fails, you'll have to spin up new instances. Generally, these need `n1-standard-8` types or 8 vCPUs/30 GB memory. 5. Ensure that there's enough room left on the disk. `ncdu` is a good linux util to verify what's claming space. +## Usage + +``` +# Create a dedicated working directory for this directory of Python scripts. +mkdir ~/chromium && cd ~/chromium +# Copy the scripts from the Kibana repo to use them conveniently in the working directory +cp -r ~/path/to/kibana/x-pack/build_chromium . +# Install the OS packages, configure the environment, download the chromium source +python ./build_chromium/init.sh [arch_name] + +# Run the build script with the path to the chromium src directory, the git commit id +python ./build_chromium/build.py + +# You can add an architecture flag for ARM +python ./build_chromium/build.py arm64 +``` + +## Getting the Commit ID +Getting `` can be tricky. The best technique seems to be: +1. Create a temporary working directory and intialize yarn +2. `yarn add puppeteer # install latest puppeter` +3. Look through puppeteer's node module files to find the "chromium revision" (a custom versioning convention for Chromium). +4. Use `https://crrev.com` and look up the revision and find the git commit info. + +The official Chromium build process is poorly documented, and seems to have +breaking changes fairly regularly. The build pre-requisites, and the build +flags change over time, so it is likely that the scripts in this directory will +be out of date by the time we have to do another Chromium build. + +This document is an attempt to note all of the gotchas we've come across while +building, so that the next time we have to tinker here, we'll have a good +starting point. + ## Build args -Chromium is built via a build tool called "ninja". The build can be configured by specifying build flags either in an "args.gn" file or via commandline args. We have an "args.gn" file per platform: +A good how-to on building Chromium from source is +[here](https://chromium.googlesource.com/chromium/src/+/master/docs/get_the_code.md). + +There are documents for each OS that will explain how to customize arguments +for the build using the `gn` tool. Those instructions do not apply for the +Kibana Chromium build. Our `build.py` script ensure the correct `args.gn` +file gets used for build arguments. -- mac: darwin/args.gn -- linux 64bit: linux-x64/args.gn +We have an `args.gn` file per platform: + +- mac: `darwin/args.gn` +- linux 64bit: `linux-x64/args.gn` +- windows: `windows/args.gn` - ARM 64bit: linux-aarch64/args.gn -- windows: windows/args.gn -The various build flags are not well documented. Some are documented [here](https://www.chromium.org/developers/gn-build-configuration). Some, such as `enable_basic_printing = false`, I only found by poking through 3rd party build scripts. +To get a list of the build arguments that are enabled, install `depot_tools` and run +`gn args out/headless --list`. It prints out all of the flags and their +settings, including the defaults. + +The various build flags are not well documented. Some are documented +[here](https://www.chromium.org/developers/gn-build-configuration). -As of this writing, there is an officially supported headless Chromium build args file for Linux: `build/args/headless.gn`. This does not work on Windows or Mac, so we have taken that as our starting point, and modified it until the Windows / Mac builds succeeded. +As of this writing, there is an officially supported headless Chromium build +args file for Linux: `build/args/headless.gn`. This does not work on Windows or +Mac, so we have taken that as our starting point, and modified it until the +Windows / Mac builds succeeded. **NOTE:** Please, make sure you consult @elastic/kibana-security before you change, remove or add any of the build flags. +## Building locally + +You can skip the step of running `/init.sh` for your OS if you already +have your environment set up, and the chromium source cloned. + +To get the Chromium code, refer to the [documentation](https://chromium.googlesource.com/chromium/src/+/master/docs/get_the_code.md). +Install `depot_tools` as suggested, since it comes with useful scripts. Use the +`fetch` command to clone the chromium repository. To set up and run the build, +use the Kibana `build.py` script (in this directory). + +It's recommended that you create a working directory for the chromium source +code and all the build tools, and run the commands from there: +``` +mkdir ~/chromium && cd ~/chromium +cp -r ~/path/to/kibana/x-pack/build_chromium . +python ./build_chromium/init.sh [arch_name] +python ./build_chromium/build.py +``` + ## VMs I ran Linux and Windows VMs in GCP with the following specs: @@ -57,7 +127,8 @@ The more cores the better, as the build makes effective use of each. For Linux, ## Initializing each VM / environment -You only need to initialize each environment once. NOTE: on Mac OS you'll need to install XCode and accept the license agreement. +In a VM, you'll want to use the init scripts to to initialize each environment. +On Mac OS you'll need to install XCode and accept the license agreement. Create the build folder: @@ -86,16 +157,6 @@ In windows, at least, you will need to do a number of extra steps: ## Building -Find the sha of the Chromium commit you wish to build. Most likely, you want to build the Chromium revision that is tied to the version of puppeteer that we're using. - -Find the Chromium revision (run in kibana's working directory): - -- `cat node_modules/puppeteer-core/package.json | grep chromium_revision` -- Take the revision number from that, and tack it to the end of this URL: https://crrev.com - - (For example, puppeteer@1.19.0 has rev (674921): https://crrev.com/674921) -- Grab the SHA from there - - (For example, rev 674921 has sha 312d84c8ce62810976feda0d3457108a6dfff9e6) - Note: In Linux, you should run the build command in tmux so that if your ssh session disconnects, the build can keep going. To do this, just type `tmux` into your terminal to hop into a tmux session. If you get disconnected, you can hop back in like so: - SSH into the server diff --git a/x-pack/build_chromium/build.py b/x-pack/build_chromium/build.py index 52ba325d6f726..8622f4a9d4c0b 100644 --- a/x-pack/build_chromium/build.py +++ b/x-pack/build_chromium/build.py @@ -1,55 +1,80 @@ -import subprocess, os, sys, platform, zipfile, hashlib, shutil -from build_util import runcmd, mkdir, md5_file, script_dir, root_dir, configure_environment +import os, subprocess, sys, platform, zipfile, hashlib, shutil +from os import path +from build_util import ( + runcmd, + runcmdsilent, + mkdir, + md5_file, + configure_environment, +) # This file builds Chromium headless on Windows, Mac, and Linux. # Verify that we have an argument, and if not print instructions if (len(sys.argv) < 2): print('Usage:') - print('python build.py {chromium_version}') + print('python build.py {chromium_version} [arch_name]') print('Example:') print('python build.py 68.0.3440.106') print('python build.py 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479') + print('python build.py 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479 arm64 # build for ARM architecture') + print sys.exit(1) +src_path = path.abspath(path.join(os.curdir, 'chromium', 'src')) +build_path = path.abspath(path.join(src_path, '..', '..')) +build_chromium_path = path.abspath(path.dirname(__file__)) +argsgn_file = path.join(build_chromium_path, platform.system().lower(), 'args.gn') + # The version of Chromium we wish to build. This can be any valid git # commit, tag, or branch, so: 68.0.3440.106 or # 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479 source_version = sys.argv[1] +base_version = source_version[:7].strip('.') # Set to "arm" to build for ARM on Linux arch_name = sys.argv[2] if len(sys.argv) >= 3 else 'x64' -print('Building Chromium ' + source_version + ' for ' + arch_name) - -# Set the environment variables required by the build tools -print('Configuring the build environment') -configure_environment() - -# Sync the codebase to the correct version, syncing master first -# to ensure that we actually have all the versions we may refer to -print('Syncing source code') - -os.chdir(os.path.join(root_dir, 'chromium/src')) - -runcmd('git checkout master') -runcmd('git fetch origin') -runcmd('gclient sync --with_branch_heads --with_tags --jobs 16') -runcmd('git checkout ' + source_version) -runcmd('gclient sync --with_branch_heads --with_tags --jobs 16') -runcmd('gclient runhooks') +if arch_name != 'x64' and arch_name != 'arm64': + raise Exception('Unexpected architecture: ' + arch_name) + +print('Building Chromium ' + source_version + ' for ' + arch_name + ' from ' + src_path) +print('src path: ' + src_path) +print('depot_tools path: ' + path.join(build_path, 'depot_tools')) +print('build_chromium_path: ' + build_chromium_path) +print('args.gn file: ' + argsgn_file) +print + +# Sync the codebase to the correct version +print('Setting local tracking branch') +print(' > cd ' + src_path) +os.chdir(src_path) + +checked_out = runcmdsilent('git checkout build-' + base_version) +if checked_out != 0: + print('Syncing remote version') + runcmd('git fetch origin ' + source_version) + print('Creating a new branch for tracking the source version') + runcmd('git checkout -b build-' + base_version + ' ' + source_version) + +depot_tools_path = os.path.join(build_path, 'depot_tools') +path_value = depot_tools_path + os.pathsep + os.environ['PATH'] +print('Updating PATH for depot_tools: ' + path_value) +os.environ['PATH'] = path_value +print('Updating all modules') +runcmd('gclient sync') # Copy build args/{Linux | Darwin | Windows}.gn from the root of our directory to out/headless/args.gn, -platform_build_args = os.path.join(script_dir, platform.system().lower(), 'args.gn') +argsgn_destination = path.abspath('out/headless/args.gn') print('Generating platform-specific args') -print('Copying build args: ' + platform_build_args + ' to out/headless/args.gn') mkdir('out/headless') -shutil.copyfile(platform_build_args, 'out/headless/args.gn') +print(' > cp ' + argsgn_file + ' ' + argsgn_destination) +shutil.copyfile(argsgn_file, argsgn_destination) print('Adding target_cpu to args') f = open('out/headless/args.gn', 'a') -f.write('\rtarget_cpu = "' + arch_name + '"') +f.write('\rtarget_cpu = "' + arch_name + '"\r') f.close() runcmd('gn gen out/headless') @@ -67,37 +92,38 @@ # Create the zip and generate the md5 hash using filenames like: # chromium-4747cc2-linux_x64.zip -base_filename = 'out/headless/chromium-' + source_version[:7].strip('.') + '-' + platform.system().lower() + '_' + arch_name +base_filename = 'out/headless/chromium-' + base_version + '-' + platform.system().lower() + '_' + arch_name zip_filename = base_filename + '.zip' md5_filename = base_filename + '.md5' -print('Creating ' + zip_filename) +print('Creating ' + path.join(src_path, zip_filename)) archive = zipfile.ZipFile(zip_filename, mode='w', compression=zipfile.ZIP_DEFLATED) def archive_file(name): """A little helper function to write individual files to the zip file""" - from_path = os.path.join('out/headless', name) - to_path = os.path.join('headless_shell-' + platform.system().lower() + '_' + arch_name, name) + from_path = path.join('out/headless', name) + to_path = path.join('headless_shell-' + platform.system().lower() + '_' + arch_name, name) archive.write(from_path, to_path) + return to_path # Each platform has slightly different requirements for what dependencies # must be bundled with the Chromium executable. if platform.system() == 'Linux': archive_file('headless_shell') - archive_file(os.path.join('swiftshader', 'libEGL.so')) - archive_file(os.path.join('swiftshader', 'libGLESv2.so')) + archive_file(path.join('swiftshader', 'libEGL.so')) + archive_file(path.join('swiftshader', 'libGLESv2.so')) if arch_name == 'arm64': - archive_file(os.path.join('swiftshader', 'libEGL.so')) + archive_file(path.join('swiftshader', 'libEGL.so')) elif platform.system() == 'Windows': archive_file('headless_shell.exe') archive_file('dbghelp.dll') archive_file('icudtl.dat') - archive_file(os.path.join('swiftshader', 'libEGL.dll')) - archive_file(os.path.join('swiftshader', 'libEGL.dll.lib')) - archive_file(os.path.join('swiftshader', 'libGLESv2.dll')) - archive_file(os.path.join('swiftshader', 'libGLESv2.dll.lib')) + archive_file(path.join('swiftshader', 'libEGL.dll')) + archive_file(path.join('swiftshader', 'libEGL.dll.lib')) + archive_file(path.join('swiftshader', 'libGLESv2.dll')) + archive_file(path.join('swiftshader', 'libGLESv2.dll.lib')) elif platform.system() == 'Darwin': archive_file('headless_shell') @@ -107,6 +133,6 @@ def archive_file(name): archive.close() -print('Creating ' + md5_filename) +print('Creating ' + path.join(src_path, md5_filename)) with open (md5_filename, 'w') as f: f.write(md5_file(zip_filename)) diff --git a/x-pack/build_chromium/build_util.py b/x-pack/build_chromium/build_util.py index 00ca13d32dba8..eaa94e5170d5c 100644 --- a/x-pack/build_chromium/build_util.py +++ b/x-pack/build_chromium/build_util.py @@ -1,33 +1,45 @@ -import os, hashlib +import os, hashlib, platform, sys # This file contains various utility functions used by the init and build scripts -# Compute the root build and script directory as relative to this file -script_dir = os.path.realpath(os.path.join(__file__, '..')) -root_dir = os.path.realpath(os.path.join(script_dir, '..')) +def runcmdsilent(cmd): + """Executes a string command in the shell""" + print(' > ' + cmd) + return os.system(cmd) def runcmd(cmd): """Executes a string command in the shell""" - print(cmd) + print(' > ' + cmd) result = os.system(cmd) if result != 0: raise Exception(cmd + ' returned ' + str(result)) def mkdir(dir): + print(' > mkdir -p ' + dir) """Makes a directory if it doesn't exist""" if not os.path.exists(dir): - print('mkdir -p ' + dir) return os.makedirs(dir) def md5_file(filename): """Builds a hex md5 hash of the given file""" md5 = hashlib.md5() - with open(filename, 'rb') as f: - for chunk in iter(lambda: f.read(128 * md5.block_size), b''): + with open(filename, 'rb') as f: + for chunk in iter(lambda: f.read(128 * md5.block_size), b''): md5.update(chunk) return md5.hexdigest() -def configure_environment(): - """Configures temporary environment variables required by Chromium's build""" - depot_tools_path = os.path.join(root_dir, 'depot_tools') - os.environ['PATH'] = depot_tools_path + os.pathsep + os.environ['PATH'] +def configure_environment(arch_name, build_path, src_path): + """Runs install scripts for deps, and configures temporary environment variables required by Chromium's build""" + + if platform.system() == 'Linux': + if arch_name: + print('Running sysroot install script...') + sysroot_cmd = src_path + '/build/linux/sysroot_scripts/install-sysroot.py --arch=' + arch_name + runcmd(sysroot_cmd) + print('Running install-build-deps...') + runcmd(src_path + '/build/install-build-deps.sh') + + depot_tools_path = os.path.join(build_path, 'depot_tools') + full_path = depot_tools_path + os.pathsep + os.environ['PATH'] + print('Updating PATH for depot_tools: ' + full_path) + os.environ['PATH'] = full_path diff --git a/x-pack/build_chromium/init.py b/x-pack/build_chromium/init.py index f543922f7653a..c0dd60f1cfcb0 100644 --- a/x-pack/build_chromium/init.py +++ b/x-pack/build_chromium/init.py @@ -1,38 +1,47 @@ import os, platform, sys -from build_util import runcmd, mkdir, md5_file, root_dir, configure_environment +from os import path +from build_util import runcmd, mkdir, md5_file, configure_environment # This is a cross-platform initialization script which should only be run # once per environment, and isn't intended to be run directly. You should # run the appropriate platform init script (e.g. Linux/init.sh) which will # call this once the platform-specific initialization has completed. -os.chdir(root_dir) +# Set to "arm" to build for ARM on Linux +arch_name = sys.argv[1] if len(sys.argv) >= 2 else 'x64' +build_path = path.abspath(os.curdir) +src_path = path.abspath(path.join(build_path, 'chromium', 'src')) + +if arch_name != 'x64' and arch_name != 'arm64': + raise Exception('Unexpected architecture: ' + arch_name) # Configure git +print('Configuring git globals...') runcmd('git config --global core.autocrlf false') runcmd('git config --global core.filemode false') runcmd('git config --global branch.autosetuprebase always') # Grab Chromium's custom build tools, if they aren't already installed # (On Windows, they are installed before this Python script is run) -if not os.path.isdir('depot_tools'): +# Put depot_tools on the path so we can properly run the fetch command +if not path.isdir('depot_tools'): + print('Installing depot_tools...') runcmd('git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git') +else: + print('Updating depot_tools...') + original_dir = os.curdir + os.chdir(path.join(build_path, 'depot_tools')) + runcmd('git checkout master') + runcmd('git pull origin master') + os.chdir(original_dir) -# Put depot_tools on the path so we can properly run the fetch command -configure_environment() +configure_environment(arch_name, build_path, src_path) # Fetch the Chromium source code -mkdir('chromium') -os.chdir('chromium') -runcmd('fetch chromium') - -# Build Linux deps -if platform.system() == 'Linux': - os.chdir('src') - - if len(sys.argv) >= 2: - sysroot_cmd = 'build/linux/sysroot_scripts/install-sysroot.py --arch=' + sys.argv[1] - print('Running `' + sysroot_cmd + '`') - runcmd(sysroot_cmd) - - runcmd('build/install-build-deps.sh') +chromium_dir = path.join(build_path, 'chromium') +if not path.isdir(chromium_dir): + mkdir(chromium_dir) + os.chdir(chromium_dir) + runcmd('fetch chromium') +else: + print('Directory exists: ' + chromium_dir + '. Skipping chromium fetch.') diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts index 40cc298db795a..721b8cb82f65f 100644 --- a/x-pack/examples/alerting_example/common/constants.ts +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -4,19 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertTypeParams } from '../../../plugins/alerts/common'; + export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; // always firing export const DEFAULT_INSTANCES_TO_GENERATE = 5; -export interface AlwaysFiringParams { +export interface AlwaysFiringThresholds { + small?: number; + medium?: number; + large?: number; +} +export interface AlwaysFiringParams extends AlertTypeParams { instances?: number; - thresholds?: { - small?: number; - medium?: number; - large?: number; - }; + thresholds?: AlwaysFiringThresholds; } -export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds']; +export type AlwaysFiringActionGroupIds = keyof AlwaysFiringThresholds; // Astros export enum Craft { diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index cee7ee62e3210..42ce6df6d1a6f 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -133,8 +133,10 @@ export const AlwaysFiringExpression: React.FunctionComponent< }; interface TShirtSelectorProps { - actionGroup?: ActionGroupWithCondition; - setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void; + actionGroup?: ActionGroupWithCondition; + setTShirtThreshold: ( + actionGroup: ActionGroupWithCondition + ) => void; } const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => { const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx index e4687c75fa0b7..eb682a86f5ff6 100644 --- a/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx @@ -23,7 +23,7 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { isEmpty } from 'lodash'; import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerts/common'; -import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { ALERTING_EXAMPLE_APP_ID, AlwaysFiringParams } from '../../common/constants'; type Props = RouteComponentProps & { http: CoreStart['http']; @@ -34,7 +34,7 @@ function hasCraft(state: any): state is { craft: string } { return state && state.craft; } export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => { - const [alert, setAlert] = useState(null); + const [alert, setAlert] = useState | null>(null); const [alertState, setAlertState] = useState(null); useEffect(() => { diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index 4fde4183b414e..fc837fee08b6f 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -11,6 +11,7 @@ import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID, AlwaysFiringParams, + AlwaysFiringActionGroupIds, } from '../../common/constants'; type ActionGroups = 'small' | 'medium' | 'large'; @@ -39,7 +40,8 @@ export const alertType: AlertType< AlwaysFiringParams, { count?: number }, { triggerdOnCycle: number }, - never + never, + AlwaysFiringActionGroupIds > = { id: 'example.always-firing', name: 'Always firing', diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index 27a8bfc7a53a3..938baa8b317ba 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -38,7 +38,14 @@ function getCraftFilter(craft: string) { craft === Craft.OuterSpace ? true : craft === person.craft; } -export const alertType: AlertType = { +export const alertType: AlertType< + { outerSpaceCapacity: number; craft: string; op: string }, + { peopleInSpace: number }, + { craft: string }, + never, + 'default', + 'hasLandedBackOnEarth' +> = { id: 'example.people-in-space', name: 'People In Space Right Now', actionGroups: [{ id: 'default', name: 'default' }], diff --git a/x-pack/examples/reporting_example/.eslintrc.js b/x-pack/examples/reporting_example/.eslintrc.js new file mode 100644 index 0000000000000..b267018448ba6 --- /dev/null +++ b/x-pack/examples/reporting_example/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + '@kbn/eslint/require-license-header': 'off', + }, +}; diff --git a/x-pack/examples/reporting_example/README.md b/x-pack/examples/reporting_example/README.md new file mode 100755 index 0000000000000..186a3fa37f93b --- /dev/null +++ b/x-pack/examples/reporting_example/README.md @@ -0,0 +1,33 @@ +# Example Reporting integration! + +Use this example code to understand how to add a "Generate Report" button to a +Kibana page. This simple example shows that the end-to-end functionality of +generating a screenshot report of a page just requires you to render a React +component that you import from the Reportinng plugin. + +A "reportable" Kibana page is one that has an **alternate version to show the data in a "screenshot-friendly" way**. The alternate version can be reached at a variation of the page's URL that the App team builds. + +A "screenshot-friendly" page has **all interactive features turned off**. These are typically notifications, popups, tooltips, controls, autocomplete libraries, etc. + +Turning off these features **keeps glitches out of the screenshot**, and makes the server-side headless browser **run faster and use less RAM**. + +The URL that Reporting captures is controlled by the application, is a part of +a "jobParams" object that gets passed to the React component imported from +Reporting. The job params give the app control over the end-resulting report: + +- Layout + - Page dimensions + - DOM attributes to select where the visualization container(s) is/are. The App team must add the attributes to DOM elements in their app. + - DOM events that the page fires off and signals when the rendering is done. The App team must implement triggering the DOM events around rendering the data in their app. +- Export type definition + - Processes the jobParams into output data, which is stored in Elasticsearch in the Reporting system index. + - Export type definitions are registered with the Reporting plugin at setup time. + +The existing export type definitions are PDF, PNG, and CSV. They should be +enough for nearly any use case. + +If the existing options are too limited for a future use case, the AppServices +team can assist the App team to implement a custom export type definition of +their own, and register it using the Reporting plugin API **(documentation coming soon)**. + +--- diff --git a/x-pack/examples/reporting_example/common/index.ts b/x-pack/examples/reporting_example/common/index.ts new file mode 100644 index 0000000000000..e47604bd7b823 --- /dev/null +++ b/x-pack/examples/reporting_example/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'reportingExample'; +export const PLUGIN_NAME = 'reportingExample'; diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json new file mode 100644 index 0000000000000..22768338aec37 --- /dev/null +++ b/x-pack/examples/reporting_example/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "reportingExample", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "optionalPlugins": [], + "requiredPlugins": ["reporting", "developerExamples", "navigation"] +} diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx new file mode 100644 index 0000000000000..1bb944faad3ea --- /dev/null +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from '../../../../src/core/public'; +import { StartDeps } from './types'; +import { ReportingExampleApp } from './components/app'; + +export const renderApp = ( + coreStart: CoreStart, + startDeps: StartDeps, + { appBasePath, element }: AppMountParameters +) => { + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx new file mode 100644 index 0000000000000..8f7176675f2c2 --- /dev/null +++ b/x-pack/examples/reporting_example/public/components/app.tsx @@ -0,0 +1,130 @@ +import { + EuiCard, + EuiCode, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPanel, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import React, { useEffect, useState } from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import * as Rx from 'rxjs'; +import { takeWhile } from 'rxjs/operators'; +import { CoreStart } from '../../../../../src/core/public'; +import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; +import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public'; +import { JobParamsPDF } from '../../../../plugins/reporting/server/export_types/printable_pdf/types'; + +interface ReportingExampleAppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; + reporting: ReportingStart; +} + +const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana']; + +export const ReportingExampleApp = ({ + basename, + notifications, + http, + reporting, +}: ReportingExampleAppDeps) => { + const { getDefaultLayoutSelectors, ReportingAPIClient } = reporting; + const [logos, setLogos] = useState([]); + + useEffect(() => { + Rx.timer(2200) + .pipe(takeWhile(() => logos.length < sourceLogos.length)) + .subscribe(() => { + setLogos([...sourceLogos.slice(0, logos.length + 1)]); + }); + }); + + const getPDFJobParams = (): JobParamsPDF => { + return { + layout: { + id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + selectors: getDefaultLayoutSelectors(), + }, + relativeUrls: ['/app/reportingExample#/intended-visualization'], + objectType: 'develeloperExample', + title: 'Reporting Developer Example', + }; + }; + + // Render the application DOM. + return ( + + + + + + +

Reporting Example

+
+
+ + + +

+ Use the ReportingStart.components.ScreenCapturePanel{' '} + component to add the Reporting panel to your page. +

+ + + + + + + + + + + + + +

+ The logos below are in a data-shared-items-container element + for Reporting. +

+ +
+ + {logos.map((item, index) => ( + + } + title={`Elastic ${item}`} + description="Example of a card's description. Stick to one or two sentences." + onClick={() => {}} + /> + + ))} + +
+
+
+
+
+
+
+
+ ); +}; diff --git a/x-pack/examples/reporting_example/public/index.ts b/x-pack/examples/reporting_example/public/index.ts new file mode 100644 index 0000000000000..a490cf96895be --- /dev/null +++ b/x-pack/examples/reporting_example/public/index.ts @@ -0,0 +1,6 @@ +import { ReportingExamplePlugin } from './plugin'; + +export function plugin() { + return new ReportingExamplePlugin(); +} +export { PluginSetup, PluginStart } from './types'; diff --git a/x-pack/examples/reporting_example/public/plugin.ts b/x-pack/examples/reporting_example/public/plugin.ts new file mode 100644 index 0000000000000..95b4d917f549a --- /dev/null +++ b/x-pack/examples/reporting_example/public/plugin.ts @@ -0,0 +1,41 @@ +import { + AppMountParameters, + AppNavLinkStatus, + CoreSetup, + CoreStart, + Plugin, +} from '../../../../src/core/public'; +import { PLUGIN_ID, PLUGIN_NAME } from '../common'; +import { SetupDeps, StartDeps } from './types'; + +export class ReportingExamplePlugin implements Plugin { + public setup(core: CoreSetup, { developerExamples, ...depsSetup }: SetupDeps): void { + core.application.register({ + id: PLUGIN_ID, + title: PLUGIN_NAME, + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + const [coreStart, depsStart] = (await core.getStartServices()) as [ + CoreStart, + StartDeps, + unknown + ]; + // Render the application + return renderApp(coreStart, { ...depsSetup, ...depsStart }, params); + }, + }); + + // Show the app in Developer Examples + developerExamples.register({ + appId: 'reportingExample', + title: 'Reporting integration', + description: 'Demonstrate how to put an Export button on a page and generate reports.', + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/examples/reporting_example/public/types.ts b/x-pack/examples/reporting_example/public/types.ts new file mode 100644 index 0000000000000..d574053266fae --- /dev/null +++ b/x-pack/examples/reporting_example/public/types.ts @@ -0,0 +1,16 @@ +import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import { ReportingStart } from '../../../plugins/reporting/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginStart {} + +export interface SetupDeps { + developerExamples: DeveloperExamplesSetup; +} +export interface StartDeps { + navigation: NavigationPublicPluginStart; + reporting: ReportingStart; +} diff --git a/x-pack/examples/reporting_example/tsconfig.json b/x-pack/examples/reporting_example/tsconfig.json new file mode 100644 index 0000000000000..ef727b3368b12 --- /dev/null +++ b/x-pack/examples/reporting_example/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target" + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "common/**/*.ts", + "../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../src/core/tsconfig.json" } + ] +} + diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx index 25de2f5953f31..fd1c708b80bf2 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx @@ -23,7 +23,7 @@ export type CollectConfigProps = CollectConfigPropsBase { +export class App1HelloWorldDrilldown implements Drilldown { public readonly id = APP1_HELLO_WORLD_DRILLDOWN; public readonly order = 8; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts index 058b52c78b427..a6c3ce652f2c9 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts @@ -13,13 +13,12 @@ import { KibanaURL } from '../../../../../../src/plugins/share/public'; export const APP1_TO_DASHBOARD_DRILLDOWN = 'APP1_TO_DASHBOARD_DRILLDOWN'; -type Trigger = typeof SAMPLE_APP1_CLICK_TRIGGER; type Context = SampleApp1ClickContext; -export class App1ToDashboardDrilldown extends AbstractDashboardDrilldown { +export class App1ToDashboardDrilldown extends AbstractDashboardDrilldown { public readonly id = APP1_TO_DASHBOARD_DRILLDOWN; - public readonly supportedTriggers = () => [SAMPLE_APP1_CLICK_TRIGGER] as Trigger[]; + public readonly supportedTriggers = () => [SAMPLE_APP1_CLICK_TRIGGER]; protected async getURL(config: Config, context: Context): Promise { const path = await this.urlGenerator.createUrl({ diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts index 33bf54d4b4cc2..9a59a715be5f8 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts @@ -13,13 +13,12 @@ import { KibanaURL } from '../../../../../../src/plugins/share/public'; export const APP2_TO_DASHBOARD_DRILLDOWN = 'APP2_TO_DASHBOARD_DRILLDOWN'; -type Trigger = typeof SAMPLE_APP2_CLICK_TRIGGER; type Context = SampleApp2ClickContext; -export class App2ToDashboardDrilldown extends AbstractDashboardDrilldown { +export class App2ToDashboardDrilldown extends AbstractDashboardDrilldown { public readonly id = APP2_TO_DASHBOARD_DRILLDOWN; - public readonly supportedTriggers = () => [SAMPLE_APP2_CLICK_TRIGGER] as Trigger[]; + public readonly supportedTriggers = () => [SAMPLE_APP2_CLICK_TRIGGER]; protected async getURL(config: Config, context: Context): Promise { const path = await this.urlGenerator.createUrl({ diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_drilldown/index.tsx index 50ad350cd90b9..8c90b38358fd6 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_drilldown/index.tsx @@ -23,8 +23,7 @@ export type Config = { const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN'; -export class DashboardHelloWorldDrilldown - implements Drilldown { +export class DashboardHelloWorldDrilldown implements Drilldown { public readonly id = SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN; public readonly order = 6; @@ -33,7 +32,7 @@ export class DashboardHelloWorldDrilldown public readonly euiIcon = 'cheer'; - supportedTriggers(): Array { + supportedTriggers(): string[] { return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_only_range_select_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_only_range_select_drilldown/index.tsx index 4e5b3187af42b..cac454d747318 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_only_range_select_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_hello_world_only_range_select_drilldown/index.tsx @@ -23,7 +23,7 @@ const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT'; export class DashboardHelloWorldOnlyRangeSelectDrilldown - implements Drilldown { + implements Drilldown { public readonly id = SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT; public readonly order = 7; @@ -57,7 +57,7 @@ export class DashboardHelloWorldOnlyRangeSelectDrilldown public readonly isConfigValid = ( config: Config, - context: BaseActionFactoryContext + context: BaseActionFactoryContext ): config is Config => { // eslint-disable-next-line no-console console.log('Showcasing, that can access action factory context:', context); diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/drilldown.tsx index 2f161efe6f388..d876143a036fd 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/drilldown.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/drilldown.tsx @@ -13,7 +13,10 @@ import { CollectConfigContainer } from './collect_config_container'; import { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../plugins/ui_actions_enhanced/public'; import { txtGoToDiscover } from './i18n'; -import { APPLY_FILTER_TRIGGER } from '../../../../../../src/plugins/data/public'; +import { + ApplyGlobalFilterActionContext, + APPLY_FILTER_TRIGGER, +} from '../../../../../../src/plugins/data/public'; const isOutputWithIndexPatterns = ( output: unknown @@ -27,7 +30,7 @@ export interface Params { } export class DashboardToDiscoverDrilldown - implements Drilldown { + implements Drilldown { constructor(protected readonly params: Params) {} public readonly id = SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app1_trigger.ts b/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app1_trigger.ts index 93a985626c6cd..aaec3f6b16ea5 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app1_trigger.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app1_trigger.ts @@ -8,18 +8,12 @@ import { Trigger } from '../../../../../src/plugins/ui_actions/public'; export const SAMPLE_APP1_CLICK_TRIGGER = 'SAMPLE_APP1_CLICK_TRIGGER'; -export const sampleApp1ClickTrigger: Trigger<'SAMPLE_APP1_CLICK_TRIGGER'> = { +export const sampleApp1ClickTrigger: Trigger = { id: SAMPLE_APP1_CLICK_TRIGGER, title: 'App 1 trigger fired on click', description: 'Could be a click on a ML job in ML app.', }; -declare module '../../../../../src/plugins/ui_actions/public' { - export interface TriggerContextMapping { - [SAMPLE_APP1_CLICK_TRIGGER]: SampleApp1ClickContext; - } -} - export interface SampleApp1ClickContext { job: SampleMlJob; } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app2_trigger.ts b/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app2_trigger.ts index 664c99afc94a5..f8e214cf7d440 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app2_trigger.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/triggers/sample_app2_trigger.ts @@ -8,18 +8,12 @@ import { Trigger } from '../../../../../src/plugins/ui_actions/public'; export const SAMPLE_APP2_CLICK_TRIGGER = 'SAMPLE_APP2_CLICK_TRIGGER'; -export const sampleApp2ClickTrigger: Trigger<'SAMPLE_APP2_CLICK_TRIGGER'> = { +export const sampleApp2ClickTrigger: Trigger = { id: SAMPLE_APP2_CLICK_TRIGGER, title: 'App 2 trigger fired on click', description: 'Could be a click on an element in Canvas app.', }; -declare module '../../../../../src/plugins/ui_actions/public' { - export interface TriggerContextMapping { - [SAMPLE_APP2_CLICK_TRIGGER]: SampleApp2ClickContext; - } -} - export interface SampleApp2ClickContext { workpadId: string; elementId: string; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index cf4ace99ed5dc..4afbbb3a33615 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -139,10 +139,11 @@ interface GetActionTypeParams { } // action type definition +export const ActionTypeId = '.email'; export function getActionType(params: GetActionTypeParams): EmailActionType { const { logger, publicBaseUrl, configurationUtilities } = params; return { - id: '.email', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.emailTitle', { defaultMessage: 'Email', diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 6926c826f776e..1b739b1567c6d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -39,11 +39,11 @@ const ParamsSchema = schema.object({ documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), }); -export const ES_INDEX_ACTION_TYPE_ID = '.index'; +export const ActionTypeId = '.index'; // action type definition export function getActionType({ logger }: { logger: Logger }): ESIndexActionType { return { - id: ES_INDEX_ACTION_TYPE_ID, + id: ActionTypeId, minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.esIndexTitle', { defaultMessage: 'Index', diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index c2058d63683bf..3a01b875ec4a0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -18,6 +18,34 @@ import { getActionType as getServiceNowActionType } from './servicenow'; import { getActionType as getJiraActionType } from './jira'; import { getActionType as getResilientActionType } from './resilient'; import { getActionType as getTeamsActionType } from './teams'; +export { ActionParamsType as EmailActionParams, ActionTypeId as EmailActionTypeId } from './email'; +export { + ActionParamsType as IndexActionParams, + ActionTypeId as IndexActionTypeId, +} from './es_index'; +export { + ActionParamsType as PagerDutyActionParams, + ActionTypeId as PagerDutyActionTypeId, +} from './pagerduty'; +export { + ActionParamsType as ServerLogActionParams, + ActionTypeId as ServerLogActionTypeId, +} from './server_log'; +export { ActionParamsType as SlackActionParams, ActionTypeId as SlackActionTypeId } from './slack'; +export { + ActionParamsType as WebhookActionParams, + ActionTypeId as WebhookActionTypeId, +} from './webhook'; +export { + ActionParamsType as ServiceNowActionParams, + ActionTypeId as ServiceNowActionTypeId, +} from './servicenow'; +export { ActionParamsType as JiraActionParams, ActionTypeId as JiraActionTypeId } from './jira'; +export { + ActionParamsType as ResilientActionParams, + ActionTypeId as ResilientActionTypeId, +} from './resilient'; +export { ActionParamsType as TeamsActionParams, ActionTypeId as TeamsActionTypeId } from './teams'; export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 4518fa0f119d5..d701fad0e0c2f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -5,7 +5,7 @@ */ import { curry } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { validate } from './validators'; import { @@ -32,6 +32,7 @@ import { import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; +export type ActionParamsType = TypeOf; interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -47,6 +48,7 @@ const supportedSubActions: string[] = [ 'issue', ]; +export const ActionTypeId = '.jira'; // action type definition export function getActionType( params: GetActionTypeParams @@ -58,7 +60,7 @@ export function getActionType( > { const { logger, configurationUtilities } = params; return { - id: '.jira', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.NAME, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 30144416491dd..7e8770ffbd629 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -583,6 +583,19 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities' ); }); + + test('it should throw an auth error', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore this can happen! + error.response = { data: 'Unauthorized' }; + throw error; + }); + + await expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Unauthorized' + ); + }); }); describe('getIssueTypes', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index f507893365c8a..f5e1b2e4411e3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -102,10 +102,14 @@ export const createExternalService = ( return fields; }; - const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { + const createErrorMessage = (errorResponse: ResponseError | string | null | undefined): string => { if (errorResponse == null) { return ''; } + if (typeof errorResponse === 'string') { + // Jira error.response.data can be string!! + return errorResponse; + } const { errorMessages, errors } = errorResponse; diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 4574b748e6014..ccd25da2397bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -117,6 +117,7 @@ function validateParams(paramsObject: unknown): string | void { } } +export const ActionTypeId = '.pagerduty'; // action type definition export function getActionType({ logger, @@ -126,7 +127,7 @@ export function getActionType({ configurationUtilities: ActionsConfigurationUtilities; }): PagerDutyActionType { return { - id: '.pagerduty', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.pagerdutyTitle', { defaultMessage: 'PagerDuty', diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index 7ce9369289554..fca99f81d62bd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -5,7 +5,7 @@ */ import { curry } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { validate } from './validators'; import { @@ -30,6 +30,8 @@ import { import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; +export type ActionParamsType = TypeOf; + interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -37,6 +39,7 @@ interface GetActionTypeParams { const supportedSubActions: string[] = ['getFields', 'pushToService', 'incidentTypes', 'severity']; +export const ActionTypeId = '.resilient'; // action type definition export function getActionType( params: GetActionTypeParams @@ -48,7 +51,7 @@ export function getActionType( > { const { logger, configurationUtilities } = params; return { - id: '.resilient', + id: ActionTypeId, minimumLicenseRequired: 'platinum', name: i18n.NAME, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index c485de8628f14..4cfea6aa9d889 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -38,11 +38,11 @@ const ParamsSchema = schema.object({ ), }); -export const SERVER_LOG_ACTION_TYPE_ID = '.server-log'; +export const ActionTypeId = '.server-log'; // action type definition export function getActionType({ logger }: { logger: Logger }): ServerLogActionType { return { - id: SERVER_LOG_ACTION_TYPE_ID, + id: ActionTypeId, minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.serverLogTitle', { defaultMessage: 'Server log', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 3fa8b25b86e8b..1f75d439200e3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -5,7 +5,7 @@ */ import { curry } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { validate } from './validators'; import { @@ -29,11 +29,14 @@ import { ServiceNowExecutorResultData, } from './types'; +export type ActionParamsType = TypeOf; + interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; } +export const ActionTypeId = '.servicenow'; // action type definition export function getActionType( params: GetActionTypeParams @@ -45,7 +48,7 @@ export function getActionType( > { const { logger, configurationUtilities } = params; return { - id: '.servicenow', + id: ActionTypeId, minimumLicenseRequired: 'platinum', name: i18n.NAME, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index a9155c329c175..c9a3c39afd049 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -52,6 +52,7 @@ const ParamsSchema = schema.object({ // action type definition +export const ActionTypeId = '.slack'; // customizing executor is only used for tests export function getActionType({ logger, @@ -63,7 +64,7 @@ export function getActionType({ executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; }): SlackActionType { return { - id: '.slack', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.slackTitle', { defaultMessage: 'Slack', diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.ts index e152a65217ce2..8575ae75d1e6c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.ts @@ -42,6 +42,7 @@ const ParamsSchema = schema.object({ message: schema.string({ minLength: 1 }), }); +export const ActionTypeId = '.teams'; // action type definition export function getActionType({ logger, @@ -51,7 +52,7 @@ export function getActionType({ configurationUtilities: ActionsConfigurationUtilities; }): TeamsActionType { return { - id: '.teams', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.teamsTitle', { defaultMessage: 'Microsoft Teams', diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 3d872d6e7e311..4479f7c69bebb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -71,6 +71,7 @@ const ParamsSchema = schema.object({ body: schema.maybe(schema.string()), }); +export const ActionTypeId = '.webhook'; // action type definition export function getActionType({ logger, @@ -80,7 +81,7 @@ export function getActionType({ configurationUtilities: ActionsConfigurationUtilities; }): WebhookActionType { return { - id: '.webhook', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.webhookTitle', { defaultMessage: 'Webhook', @@ -176,8 +177,14 @@ export async function executor( const { error } = result; if (error.response) { - const { status, statusText, headers: responseHeaders } = error.response; - const message = `[${status}] ${statusText}`; + const { + status, + statusText, + headers: responseHeaders, + data: { message: responseMessage }, + } = error.response; + const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : ''; + const message = `[${status}] ${statusText}${responseMessageAsSuffix}`; logger.error(`error on ${actionId} webhook event: ${message}`); // The request was made and the server responded with a status code // that falls out of the range of 2xx @@ -195,6 +202,10 @@ export async function executor( ); } return errorResultInvalid(actionId, message); + } else if (error.isAxiosError) { + const message = `[${error.code}] ${error.message}`; + logger.error(`error on ${actionId} webhook event: ${message}`); + return errorResultRequestFailed(actionId, message); } logger.error(`error on ${actionId} webhook action: unexpected error`); @@ -222,6 +233,21 @@ function errorResultInvalid( }; } +function errorResultRequestFailed( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { + const errMessage = i18n.translate('xpack.actions.builtin.webhook.requestFailedErrorMessage', { + defaultMessage: 'error calling webhook, request failed', + }); + return { + status: 'error', + message: errMessage, + actionId, + serviceMessage, + }; +} + function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.webhook.unreachableErrorMessage', { defaultMessage: 'error calling webhook, unexpected error', diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 39bfe2c2820e2..c43cc20bd4773 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -21,6 +21,30 @@ export { ActionType, PreConfiguredAction, } from './types'; + +export type { + EmailActionTypeId, + EmailActionParams, + IndexActionTypeId, + IndexActionParams, + PagerDutyActionTypeId, + PagerDutyActionParams, + ServerLogActionTypeId, + ServerLogActionParams, + SlackActionTypeId, + SlackActionParams, + WebhookActionTypeId, + WebhookActionParams, + ServiceNowActionTypeId, + ServiceNowActionParams, + JiraActionTypeId, + JiraActionParams, + ResilientActionTypeId, + ResilientActionParams, + TeamsActionTypeId, + TeamsActionParams, +} from './builtin_action_types'; + export { PluginSetupContract, PluginStartContract } from './plugin'; export { asSavedObjectExecutionSource, asHttpRequestExecutionSource } from './lib'; diff --git a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts index 0f309bb76b76c..f22e87a58ec7f 100644 --- a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts +++ b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts @@ -5,14 +5,13 @@ */ import { ActionType } from '../types'; import { LICENSE_TYPE } from '../../../licensing/common/types'; -import { SERVER_LOG_ACTION_TYPE_ID } from '../builtin_action_types/server_log'; -import { ES_INDEX_ACTION_TYPE_ID } from '../builtin_action_types/es_index'; +import { ServerLogActionTypeId, IndexActionTypeId } from '../builtin_action_types'; import { CASE_ACTION_TYPE_ID } from '../../../case/server'; import { ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; const ACTIONS_SCOPED_WITHIN_STACK = new Set([ - SERVER_LOG_ACTION_TYPE_ID, - ES_INDEX_ACTION_TYPE_ID, + ServerLogActionTypeId, + IndexActionTypeId, CASE_ACTION_TYPE_ID, ]); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index b311a602212c7..81d6c3550a53c 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -31,6 +31,9 @@ export type ActionTypeSecrets = Record; export type ActionTypeParams = Record; export interface Services { + /** + * @deprecated Use `scopedClusterClient` instead. + */ callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; scopedClusterClient: ElasticsearchClient; diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 39dc23c7bbb73..2191b23eec11e 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -142,8 +142,41 @@ This example receives server and threshold as parameters. It will read the CPU u ```typescript import { schema } from '@kbn/config-schema'; +import { + Alert, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext +} from 'x-pack/plugins/alerts/common'; ... -server.newPlatform.setup.plugins.alerts.registerType({ +interface MyAlertTypeParams extends AlertTypeParams { + server: string; + threshold: number; +} + +interface MyAlertTypeState extends AlertTypeState { + lastChecked: number; +} + +interface MyAlertTypeInstanceState extends AlertInstanceState { + cpuUsage: number; +} + +interface MyAlertTypeInstanceContext extends AlertInstanceContext { + server: string; + hasCpuUsageIncreased: boolean; +} + +type MyAlertTypeActionGroups = 'default' | 'warning'; + +const myAlertType: AlertType< + MyAlertTypeParams, + MyAlertTypeState, + MyAlertTypeInstanceState, + MyAlertTypeInstanceContext, + MyAlertTypeActionGroups +> = { id: 'my-alert-type', name: 'My alert type', validate: { @@ -180,7 +213,7 @@ server.newPlatform.setup.plugins.alerts.registerType({ services, params, state, - }: AlertExecutorOptions) { + }: AlertExecutorOptions) { // Let's assume params is { server: 'server_1', threshold: 0.8 } const { server, threshold } = params; @@ -219,7 +252,9 @@ server.newPlatform.setup.plugins.alerts.registerType({ }; }, producer: 'alerting', -}); +}; + +server.newPlatform.setup.plugins.alerts.registerType(myAlertType); ``` This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server. diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts index d74f66898eff6..ed3fbcf2ddc9b 100644 --- a/x-pack/plugins/alerts/common/alert.ts +++ b/x-pack/plugins/alerts/common/alert.ts @@ -7,10 +7,8 @@ import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server'; import { AlertNotifyWhenType } from './alert_notify_when_type'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AlertTypeState = Record; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AlertTypeParams = Record; +export type AlertTypeState = Record; +export type AlertTypeParams = Record; export interface IntervalSchedule extends SavedObjectAttributes { interval: string; @@ -52,7 +50,7 @@ export interface AlertAggregations { alertExecutionStatus: { [status: string]: number }; } -export interface Alert { +export interface Alert { id: string; enabled: boolean; name: string; @@ -61,7 +59,7 @@ export interface Alert { consumer: string; schedule: IntervalSchedule; actions: AlertAction[]; - params: AlertTypeParams; + params: Params; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; @@ -76,7 +74,7 @@ export interface Alert { executionStatus: AlertExecutionStatus; } -export type SanitizedAlert = Omit; +export type SanitizedAlert = Omit, 'apiKey'>; export enum HealthStatus { OK = 'ok', diff --git a/x-pack/plugins/alerts/common/alert_type.ts b/x-pack/plugins/alerts/common/alert_type.ts index 4ab3ddc7ca810..d10d2467516cc 100644 --- a/x-pack/plugins/alerts/common/alert_type.ts +++ b/x-pack/plugins/alerts/common/alert_type.ts @@ -5,19 +5,29 @@ */ import { LicenseType } from '../../licensing/common/types'; +import { RecoveredActionGroupId, DefaultActionGroupId } from './builtin_action_groups'; -export interface AlertType { +export interface AlertType< + ActionGroupIds extends Exclude = DefaultActionGroupId, + RecoveryActionGroupId extends string = RecoveredActionGroupId +> { id: string; name: string; - actionGroups: ActionGroup[]; - recoveryActionGroup: ActionGroup; + actionGroups: Array>; + recoveryActionGroup: ActionGroup; actionVariables: string[]; - defaultActionGroupId: ActionGroup['id']; + defaultActionGroupId: ActionGroupIds; producer: string; minimumLicenseRequired: LicenseType; } -export interface ActionGroup { - id: string; +export interface ActionGroup { + id: ActionGroupIds; name: string; } + +export type ActionGroupIdsOf = T extends ActionGroup + ? groups + : T extends Readonly> + ? groups + : never; diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts index e23bbcc54b24d..f2b7ec855b86e 100644 --- a/x-pack/plugins/alerts/common/builtin_action_groups.ts +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -6,13 +6,27 @@ import { i18n } from '@kbn/i18n'; import { ActionGroup } from './alert_type'; -export const RecoveredActionGroup: Readonly = { +export type DefaultActionGroupId = 'default'; + +export type RecoveredActionGroupId = typeof RecoveredActionGroup['id']; +export const RecoveredActionGroup: Readonly> = Object.freeze({ id: 'recovered', name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', { defaultMessage: 'Recovered', }), -}; +}); + +export type ReservedActionGroups = + | RecoveryActionGroupId + | RecoveredActionGroupId; + +export type WithoutReservedActionGroups< + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> = ActionGroupIds extends ReservedActionGroups ? never : ActionGroupIds; -export function getBuiltinActionGroups(customRecoveryGroup?: ActionGroup): ActionGroup[] { - return [customRecoveryGroup ?? Object.freeze(RecoveredActionGroup)]; +export function getBuiltinActionGroups( + customRecoveryGroup?: ActionGroup +): [ActionGroup>] { + return [customRecoveryGroup ?? RecoveredActionGroup]; } diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts index b428f6c1a9134..1bd08fc3ac32d 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts @@ -6,6 +6,7 @@ import sinon from 'sinon'; import { AlertInstance } from './alert_instance'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; let clock: sinon.SinonFakeTimers; @@ -17,12 +18,20 @@ afterAll(() => clock.restore()); describe('hasScheduledActions()', () => { test('defaults to false', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.hasScheduledActions()).toEqual(false); }); test('returns true when scheduleActions is called', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.hasScheduledActions()).toEqual(true); }); @@ -30,7 +39,11 @@ describe('hasScheduledActions()', () => { describe('isThrottled', () => { test(`should throttle when group didn't change and throttle period is still active`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -44,7 +57,11 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group didn't change and throttle period expired`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -58,7 +75,7 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group changes`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -74,12 +91,20 @@ describe('isThrottled', () => { describe('scheduledActionGroupOrSubgroupHasChanged()', () => { test('should be false if no last scheduled and nothing scheduled', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); }); test('should be false if group does not change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -92,7 +117,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group and subgroup does not change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -106,7 +135,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -119,7 +152,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -133,13 +170,17 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if no last scheduled and has scheduled action', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); }); test('should be true if group does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -152,7 +193,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does change and subgroup does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -166,7 +207,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does not change and subgroup does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -182,14 +227,22 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { describe('getScheduledActionOptions()', () => { test('defaults to undefined', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); }); }); describe('unscheduleActions()', () => { test('makes hasScheduledActions() return false', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.hasScheduledActions()).toEqual(true); alertInstance.unscheduleActions(); @@ -197,7 +250,11 @@ describe('unscheduleActions()', () => { }); test('makes getScheduledActionOptions() return undefined', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.getScheduledActionOptions()).toEqual({ actionGroup: 'default', @@ -212,14 +269,22 @@ describe('unscheduleActions()', () => { describe('getState()', () => { test('returns state passed to constructor', () => { const state = { foo: true }; - const alertInstance = new AlertInstance({ state }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state }); expect(alertInstance.getState()).toEqual(state); }); }); describe('scheduleActions()', () => { test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -233,7 +298,11 @@ describe('scheduleActions()', () => { }); test('makes isThrottled() return true when throttled', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -247,7 +316,11 @@ describe('scheduleActions()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -262,7 +335,11 @@ describe('scheduleActions()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance({ state: { foo: true }, meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: {} }); alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); expect(alertInstance.getScheduledActionOptions()).toEqual({ actionGroup: 'default', @@ -272,7 +349,11 @@ describe('scheduleActions()', () => { }); test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default', { field: true }); expect(() => alertInstance.scheduleActions('default', { field: false }) @@ -284,7 +365,11 @@ describe('scheduleActions()', () => { describe('scheduleActionsWithSubGroup()', () => { test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -300,7 +385,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -317,7 +406,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -333,7 +426,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -350,7 +447,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -367,7 +468,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance({ state: { foo: true }, meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: {} }); alertInstance .replaceState({ otherField: true }) .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); @@ -380,7 +485,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -390,7 +499,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice with different subgroups', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -400,7 +513,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice whether there are subgroups', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -412,7 +529,11 @@ describe('scheduleActionsWithSubGroup()', () => { describe('replaceState()', () => { test('replaces previous state', () => { - const alertInstance = new AlertInstance({ state: { foo: true } }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true } }); alertInstance.replaceState({ bar: true }); expect(alertInstance.getState()).toEqual({ bar: true }); alertInstance.replaceState({ baz: true }); @@ -422,7 +543,11 @@ describe('replaceState()', () => { describe('updateLastScheduledActions()', () => { test('replaces previous lastScheduledActions', () => { - const alertInstance = new AlertInstance({ meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: {} }); alertInstance.updateLastScheduledActions('default'); expect(alertInstance.toJSON()).toEqual({ state: {}, @@ -438,7 +563,11 @@ describe('updateLastScheduledActions()', () => { describe('toJSON', () => { test('only serializes state and meta', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -464,7 +593,11 @@ describe('toRaw', () => { }, }, }; - const alertInstance = new AlertInstance(raw); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(raw); expect(alertInstance.toRaw()).toEqual(raw); }); }); diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts index 8841f3115d547..c49b38e157a07 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts @@ -9,15 +9,17 @@ import { RawAlertInstance, rawAlertInstance, AlertInstanceContext, + DefaultActionGroupId, } from '../../common'; import { parseDuration } from '../lib'; interface ScheduledExecutionOptions< State extends AlertInstanceState, - Context extends AlertInstanceContext + Context extends AlertInstanceContext, + ActionGroupIds extends string = DefaultActionGroupId > { - actionGroup: string; + actionGroup: ActionGroupIds; subgroup?: string; context: Context; state: State; @@ -25,17 +27,19 @@ interface ScheduledExecutionOptions< export type PublicAlertInstance< State extends AlertInstanceState = AlertInstanceState, - Context extends AlertInstanceContext = AlertInstanceContext + Context extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = DefaultActionGroupId > = Pick< - AlertInstance, + AlertInstance, 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' >; export class AlertInstance< State extends AlertInstanceState = AlertInstanceState, - Context extends AlertInstanceContext = AlertInstanceContext + Context extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never > { - private scheduledExecutionOptions?: ScheduledExecutionOptions; + private scheduledExecutionOptions?: ScheduledExecutionOptions; private meta: AlertInstanceMeta; private state: State; @@ -97,14 +101,14 @@ export class AlertInstance< private scheduledActionGroupIsUnchanged( lastScheduledActions: NonNullable, - scheduledExecutionOptions: ScheduledExecutionOptions + scheduledExecutionOptions: ScheduledExecutionOptions ) { return lastScheduledActions.group === scheduledExecutionOptions.actionGroup; } private scheduledActionSubgroupIsUnchanged( lastScheduledActions: NonNullable, - scheduledExecutionOptions: ScheduledExecutionOptions + scheduledExecutionOptions: ScheduledExecutionOptions ) { return lastScheduledActions.subgroup && scheduledExecutionOptions.subgroup ? lastScheduledActions.subgroup === scheduledExecutionOptions.subgroup @@ -128,7 +132,7 @@ export class AlertInstance< return this.state; } - scheduleActions(actionGroup: string, context: Context = {} as Context) { + scheduleActions(actionGroup: ActionGroupIds, context: Context = {} as Context) { this.ensureHasNoScheduledActions(); this.scheduledExecutionOptions = { actionGroup, @@ -139,7 +143,7 @@ export class AlertInstance< } scheduleActionsWithSubGroup( - actionGroup: string, + actionGroup: ActionGroupIds, subgroup: string, context: Context = {} as Context ) { @@ -164,7 +168,7 @@ export class AlertInstance< return this; } - updateLastScheduledActions(group: string, subgroup?: string) { + updateLastScheduledActions(group: ActionGroupIds, subgroup?: string) { this.meta.lastScheduledActions = { group, subgroup, date: new Date() }; } diff --git a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts index 0b29262ddcc07..6ba4a8b57d9de 100644 --- a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts +++ b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts @@ -4,12 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertInstanceContext, AlertInstanceState } from '../types'; import { AlertInstance } from './alert_instance'; -export function createAlertInstanceFactory(alertInstances: Record) { - return (id: string): AlertInstance => { +export function createAlertInstanceFactory< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +>(alertInstances: Record>) { + return (id: string): AlertInstance => { if (!alertInstances[id]) { - alertInstances[id] = new AlertInstance(); + alertInstances[id] = new AlertInstance(); } return alertInstances[id]; diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 58b2cb74f2353..1fdd64d56d466 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -6,7 +6,7 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertTypeRegistry, ConstructorOptions } from './alert_type_registry'; -import { AlertType } from './types'; +import { ActionGroup, AlertType } from './types'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { ILicenseState } from './lib/license_state'; import { licenseStateMock } from './lib/license_state.mock'; @@ -55,7 +55,7 @@ describe('has()', () => { describe('register()', () => { test('throws if AlertType Id contains invalid characters', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -87,7 +87,7 @@ describe('register()', () => { }); test('throws if AlertType Id isnt a string', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: (123 as unknown) as string, name: 'Test', actionGroups: [ @@ -109,7 +109,7 @@ describe('register()', () => { }); test('throws if AlertType action groups contains reserved group id', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -117,10 +117,14 @@ describe('register()', () => { id: 'default', name: 'Default', }, - { + /** + * The type system will ensure you can't use the `recovered` action group + * but we also want to ensure this at runtime + */ + ({ id: 'recovered', name: 'Recovered', - }, + } as unknown) as ActionGroup<'NotReserved'>, ], defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -137,7 +141,7 @@ describe('register()', () => { }); test('allows an AlertType to specify a custom recovery group', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -172,7 +176,14 @@ describe('register()', () => { }); test('throws if the custom recovery group is contained in the AlertType action groups', () => { - const alertType: AlertType = { + const alertType: AlertType< + never, + never, + never, + never, + 'default' | 'backToAwesome', + 'backToAwesome' + > = { id: 'test', name: 'Test', actionGroups: [ @@ -204,7 +215,7 @@ describe('register()', () => { }); test('registers the executor with the task manager', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -234,7 +245,7 @@ describe('register()', () => { }); test('shallow clones the given alert type', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -475,8 +486,12 @@ describe('ensureAlertTypeEnabled', () => { }); }); -function alertTypeWithVariables(id: string, context: string, state: string): AlertType { - const baseAlert: AlertType = { +function alertTypeWithVariables( + id: ActionGroupIds, + context: string, + state: string +): AlertType { + const baseAlert: AlertType = { id, name: `${id}-name`, actionGroups: [], diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index d436d1987c027..c26088b6bce3c 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -19,7 +19,12 @@ import { AlertInstanceState, AlertInstanceContext, } from './types'; -import { RecoveredActionGroup, getBuiltinActionGroups } from '../common'; +import { + RecoveredActionGroup, + getBuiltinActionGroups, + RecoveredActionGroupId, + ActionGroup, +} from '../common'; import { ILicenseState } from './lib/license_state'; import { getAlertTypeFeatureUsageName } from './lib/get_alert_type_feature_usage_name'; @@ -32,7 +37,7 @@ export interface ConstructorOptions { export interface RegistryAlertType extends Pick< - NormalizedAlertType, + UntypedNormalizedAlertType, | 'name' | 'actionGroups' | 'recoveryActionGroup' @@ -66,16 +71,44 @@ const alertIdSchema = schema.string({ }); export type NormalizedAlertType< - Params extends AlertTypeParams = AlertTypeParams, - State extends AlertTypeState = AlertTypeState, - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext -> = Omit, 'recoveryActionGroup'> & - Pick>, 'recoveryActionGroup'>; + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> = { + actionGroups: Array>; +} & Omit< + AlertType, + 'recoveryActionGroup' | 'actionGroups' +> & + Pick< + Required< + AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + >, + 'recoveryActionGroup' + >; + +export type UntypedNormalizedAlertType = NormalizedAlertType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string, + string +>; export class AlertTypeRegistry { private readonly taskManager: TaskManagerSetupContract; - private readonly alertTypes: Map = new Map(); + private readonly alertTypes: Map = new Map(); private readonly taskRunnerFactory: TaskRunnerFactory; private readonly licenseState: ILicenseState; private readonly licensing: LicensingPluginSetup; @@ -96,11 +129,22 @@ export class AlertTypeRegistry { } public register< - Params extends AlertTypeParams = AlertTypeParams, - State extends AlertTypeState = AlertTypeState, - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext - >(alertType: AlertType) { + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { if (this.has(alertType.id)) { throw new Error( i18n.translate('xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError', { @@ -113,14 +157,32 @@ export class AlertTypeRegistry { } alertType.actionVariables = normalizedActionVariables(alertType.actionVariables); - const normalizedAlertType = augmentActionGroupsWithReserved(alertType as AlertType); + const normalizedAlertType = augmentActionGroupsWithReserved< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >(alertType); - this.alertTypes.set(alertIdSchema.validate(alertType.id), normalizedAlertType); + this.alertTypes.set( + alertIdSchema.validate(alertType.id), + /** stripping the typing is required in order to store the AlertTypes in a Map */ + (normalizedAlertType as unknown) as UntypedNormalizedAlertType + ); this.taskManager.registerTaskDefinitions({ [`alerting:${alertType.id}`]: { title: alertType.name, createTaskRunner: (context: RunContext) => - this.taskRunnerFactory.create(normalizedAlertType, context), + this.taskRunnerFactory.create< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId | RecoveredActionGroupId + >(normalizedAlertType, context), }, }); // No need to notify usage on basic alert types @@ -136,8 +198,19 @@ export class AlertTypeRegistry { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext - >(id: string): NormalizedAlertType { + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = string, + RecoveryActionGroupId extends string = string + >( + id: string + ): NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > { if (!this.has(id)) { throw Boom.badRequest( i18n.translate('xpack.alerts.alertTypeRegistry.get.missingAlertTypeError', { @@ -148,11 +221,18 @@ export class AlertTypeRegistry { }) ); } - return this.alertTypes.get(id)! as NormalizedAlertType< + /** + * When we store the AlertTypes in the Map we strip the typing. + * This means that returning a typed AlertType in `get` is an inherently + * unsafe operation. Down casting to `unknown` is the only way to achieve this. + */ + return (this.alertTypes.get(id)! as unknown) as NormalizedAlertType< Params, State, InstanceState, - InstanceContext + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId >; } @@ -170,7 +250,7 @@ export class AlertTypeRegistry { producer, minimumLicenseRequired, }, - ]: [string, NormalizedAlertType]) => ({ + ]: [string, UntypedNormalizedAlertType]) => ({ id, name, actionGroups, @@ -202,15 +282,31 @@ function augmentActionGroupsWithReserved< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string >( - alertType: AlertType -): NormalizedAlertType { + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > +): NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveredActionGroupId | RecoveryActionGroupId +> { const reservedActionGroups = getBuiltinActionGroups(alertType.recoveryActionGroup); const { id, actionGroups, recoveryActionGroup } = alertType; - const activeActionGroups = new Set(actionGroups.map((item) => item.id)); - const intersectingReservedActionGroups = intersection( + const activeActionGroups = new Set(actionGroups.map((item) => item.id)); + const intersectingReservedActionGroups = intersection( [...activeActionGroups.values()], reservedActionGroups.map((item) => item.id) ); diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index f21cd2b02943a..a47af44d330c3 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -23,13 +23,13 @@ import { RawAlert, AlertTypeRegistry, AlertAction, - AlertType, IntervalSchedule, SanitizedAlert, AlertTaskState, AlertInstanceSummary, AlertExecutionStatusValues, AlertNotifyWhenType, + AlertTypeParams, } from '../types'; import { validateAlertTypeParams, @@ -43,8 +43,7 @@ import { import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; -import { deleteTaskIfItExists } from '../lib/delete_task_if_it_exists'; -import { RegistryAlertType } from '../alert_type_registry'; +import { RegistryAlertType, UntypedNormalizedAlertType } from '../alert_type_registry'; import { AlertsAuthorization, WriteOperations, ReadOperations } from '../authorization'; import { IEventLogClient } from '../../../../plugins/event_log/server'; import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; @@ -127,16 +126,16 @@ interface AggregateResult { alertExecutionStatus: { [status: string]: number }; } -export interface FindResult { +export interface FindResult { page: number; perPage: number; total: number; - data: SanitizedAlert[]; + data: Array>; } -export interface CreateOptions { +export interface CreateOptions { data: Omit< - Alert, + Alert, | 'id' | 'createdBy' | 'updatedBy' @@ -154,14 +153,14 @@ export interface CreateOptions { }; } -interface UpdateOptions { +interface UpdateOptions { id: string; data: { name: string; tags: string[]; schedule: IntervalSchedule; actions: NormalizedAlertAction[]; - params: Record; + params: Params; throttle: string | null; notifyWhen: AlertNotifyWhenType | null; }; @@ -223,7 +222,10 @@ export class AlertsClient { this.auditLogger = auditLogger; } - public async create({ data, options }: CreateOptions): Promise { + public async create({ + data, + options, + }: CreateOptions): Promise> { const id = SavedObjectsUtils.generateId(); try { @@ -248,7 +250,10 @@ export class AlertsClient { // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); + const validatedAlertTypeParams = validateAlertTypeParams( + data.params, + alertType.validate?.params + ); const username = await this.getUserName(); const createdAPIKey = data.enabled @@ -334,10 +339,14 @@ export class AlertsClient { }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; } - return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references); + return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references); } - public async get({ id }: { id: string }): Promise { + public async get({ + id, + }: { + id: string; + }): Promise> { const result = await this.unsecuredSavedObjectsClient.get('alert', id); try { await this.authorization.ensureAuthorized( @@ -361,7 +370,7 @@ export class AlertsClient { savedObject: { type: 'alert', id }, }) ); - return this.getAlertFromRaw(result.id, result.attributes, result.references); + return this.getAlertFromRaw(result.id, result.attributes, result.references); } public async getAlertState({ id }: { id: string }): Promise { @@ -426,9 +435,9 @@ export class AlertsClient { }); } - public async find({ + public async find({ options: { fields, ...options } = {}, - }: { options?: FindOptions } = {}): Promise { + }: { options?: FindOptions } = {}): Promise> { let authorizationTuple; try { authorizationTuple = await this.authorization.getFindAuthorizationFilter(); @@ -475,7 +484,7 @@ export class AlertsClient { ); throw error; } - return this.getAlertFromRaw( + return this.getAlertFromRaw( id, fields ? (pick(attributes, fields) as RawAlert) : attributes, references @@ -592,7 +601,7 @@ export class AlertsClient { const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); await Promise.all([ - taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, + taskIdToRemove ? this.taskManager.removeIfExists(taskIdToRemove) : null, apiKeyToInvalidate ? markApiKeyForInvalidation( { apiKey: apiKeyToInvalidate }, @@ -605,15 +614,21 @@ export class AlertsClient { return removeResult; } - public async update({ id, data }: UpdateOptions): Promise { + public async update({ + id, + data, + }: UpdateOptions): Promise> { return await retryIfConflicts( this.logger, `alertsClient.update('${id}')`, - async () => await this.updateWithOCC({ id, data }) + async () => await this.updateWithOCC({ id, data }) ); } - private async updateWithOCC({ id, data }: UpdateOptions): Promise { + private async updateWithOCC({ + id, + data, + }: UpdateOptions): Promise> { let alertSavedObject: SavedObject; try { @@ -658,7 +673,7 @@ export class AlertsClient { this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId); - const updateResult = await this.updateAlert({ id, data }, alertSavedObject); + const updateResult = await this.updateAlert({ id, data }, alertSavedObject); await Promise.all([ alertSavedObject.attributes.apiKey @@ -692,14 +707,17 @@ export class AlertsClient { return updateResult; } - private async updateAlert( - { id, data }: UpdateOptions, + private async updateAlert( + { id, data }: UpdateOptions, { attributes, version }: SavedObject - ): Promise { + ): Promise> { const alertType = this.alertTypeRegistry.get(attributes.alertTypeId); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); + const validatedAlertTypeParams = validateAlertTypeParams( + data.params, + alertType.validate?.params + ); this.validateActions(alertType, data.actions); const { actions, references } = await this.denormalizeActions(data.actions); @@ -1041,7 +1059,7 @@ export class AlertsClient { await Promise.all([ attributes.scheduledTaskId - ? deleteTaskIfItExists(this.taskManager, attributes.scheduledTaskId) + ? this.taskManager.removeIfExists(attributes.scheduledTaskId) : null, apiKeyToInvalidate ? await markApiKeyForInvalidation( @@ -1343,7 +1361,7 @@ export class AlertsClient { }) as Alert['actions']; } - private getAlertFromRaw( + private getAlertFromRaw( id: string, rawAlert: RawAlert, references: SavedObjectReference[] | undefined @@ -1351,14 +1369,14 @@ export class AlertsClient { // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawAlert, it is safe // to cast the result to an Alert - return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert; + return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert; } - private getPartialAlertFromRaw( + private getPartialAlertFromRaw( id: string, { createdAt, updatedAt, meta, notifyWhen, scheduledTaskId, ...rawAlert }: Partial, references: SavedObjectReference[] | undefined - ): PartialAlert { + ): PartialAlert { // Not the prettiest code here, but if we want to use most of the // alert fields from the rawAlert using `...rawAlert` kind of access, we // need to specifically delete the executionStatus as it's a different type @@ -1386,7 +1404,10 @@ export class AlertsClient { }; } - private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void { + private validateActions( + alertType: UntypedNormalizedAlertType, + actions: NormalizedAlertAction[] + ): void { const { actionGroups: alertTypeActionGroups } = alertType; const usedAlertActionGroups = actions.map((action) => action.group); const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index 5f830a6c5bc51..0424a1295c9b9 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -59,7 +59,11 @@ beforeEach(() => { setGlobalDate(); -function getMockData(overwrites: Record = {}): CreateOptions['data'] { +function getMockData( + overwrites: Record = {} +): CreateOptions<{ + bar: boolean; +}>['data'] { return { enabled: true, name: 'abc', @@ -93,7 +97,11 @@ describe('create()', () => { }); describe('authorization', () => { - function tryToExecuteOperation(options: CreateOptions): Promise { + function tryToExecuteOperation( + options: CreateOptions<{ + bar: boolean; + }> + ): Promise { unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts index a7ef008eaa2ee..8022bc26742aa 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts @@ -110,7 +110,7 @@ describe('delete()', () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( 'api_key_pending_invalidation' ); @@ -135,7 +135,7 @@ describe('delete()', () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( @@ -153,7 +153,7 @@ describe('delete()', () => { }); await alertsClient.delete({ id: '1' }); - expect(taskManager.remove).not.toHaveBeenCalled(); + expect(taskManager.removeIfExists).not.toHaveBeenCalled(); }); test(`doesn't invalidate API key when apiKey is null`, async () => { @@ -217,8 +217,8 @@ describe('delete()', () => { ); }); - test('throws error when taskManager.remove throws an error', async () => { - taskManager.remove.mockRejectedValue(new Error('TM Fail')); + test('throws error when taskManager.removeIfExists throws an error', async () => { + taskManager.removeIfExists.mockRejectedValue(new Error('TM Fail')); await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"TM Fail"` diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts index ce0688a5ab2ff..448546941185b 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -199,7 +199,7 @@ describe('disable()', () => { version: '123', } ); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); expect( (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId ).toBe('123'); @@ -254,7 +254,7 @@ describe('disable()', () => { version: '123', } ); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); @@ -280,7 +280,7 @@ describe('disable()', () => { await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); - expect(taskManager.remove).not.toHaveBeenCalled(); + expect(taskManager.removeIfExists).not.toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); @@ -314,7 +314,7 @@ describe('disable()', () => { await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(taskManager.remove).toHaveBeenCalled(); + expect(taskManager.removeIfExists).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'disable(): Failed to load API key to invalidate on alert 1: Fail' @@ -338,7 +338,7 @@ describe('disable()', () => { }); test('throws when failing to remove task from task manager', async () => { - taskManager.remove.mockRejectedValueOnce(new Error('Failed to remove task')); + taskManager.removeIfExists.mockRejectedValueOnce(new Error('Failed to remove task')); await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Failed to remove task"` diff --git a/x-pack/plugins/alerts/server/index.ts b/x-pack/plugins/alerts/server/index.ts index 7bb54cd87bc33..da56da671f9b0 100644 --- a/x-pack/plugins/alerts/server/index.ts +++ b/x-pack/plugins/alerts/server/index.ts @@ -16,6 +16,7 @@ export { ActionVariable, AlertType, ActionGroup, + ActionGroupIdsOf, AlertingPlugin, AlertExecutorOptions, AlertActionParams, diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts index 91c3f5954d6d0..5e26a5776f9fe 100644 --- a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -12,7 +12,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; -import { InvalidateAPIKeyParams, SecurityPluginStart } from '../../../security/server'; +import { InvalidateAPIKeysParams, SecurityPluginStart } from '../../../security/server'; import { RunContext, TaskManagerSetupContract, @@ -27,8 +27,8 @@ import { InvalidatePendingApiKey } from '../types'; const TASK_TYPE = 'alerts_invalidate_api_keys'; export const TASK_ID = `Alerts-${TASK_TYPE}`; -const invalidateAPIKey = async ( - params: InvalidateAPIKeyParams, +const invalidateAPIKeys = async ( + params: InvalidateAPIKeysParams, securityPluginStart?: SecurityPluginStart ): Promise => { if (!securityPluginStart) { @@ -194,28 +194,34 @@ async function invalidateApiKeys( securityPluginStart?: SecurityPluginStart ) { let totalInvalidated = 0; - await Promise.all( + const apiKeyIds = await Promise.all( apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( 'api_key_pending_invalidation', apiKeyObj.id ); - const apiKeyId = decryptedApiKey.attributes.apiKeyId; - const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginStart); - if (response.apiKeysEnabled === true && response.result.error_count > 0) { - logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`); - } else { - try { - await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id); - totalInvalidated++; - } catch (err) { - logger.error( - `Failed to cleanup api key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}` - ); - } - } + return decryptedApiKey.attributes.apiKeyId; }) ); - logger.debug(`Total invalidated api keys "${totalInvalidated}"`); + if (apiKeyIds.length > 0) { + const response = await invalidateAPIKeys({ ids: apiKeyIds }, securityPluginStart); + if (response.apiKeysEnabled === true && response.result.error_count > 0) { + logger.error(`Failed to invalidate API Keys [ids="${apiKeyIds.join(', ')}"]`); + } else { + await Promise.all( + apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { + try { + await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id); + totalInvalidated++; + } catch (err) { + logger.error( + `Failed to delete invalidated API key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}` + ); + } + }) + ); + } + } + logger.debug(`Total invalidated API keys "${totalInvalidated}"`); return totalInvalidated; } diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts index d6357494546b0..0f91e5d0c24a9 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts @@ -635,11 +635,11 @@ export class EventsFactory { } } -function createAlert(overrides: Partial): SanitizedAlert { +function createAlert(overrides: Partial): SanitizedAlert<{ bar: boolean }> { return { ...BaseAlert, ...overrides }; } -const BaseAlert: SanitizedAlert = { +const BaseAlert: SanitizedAlert<{ bar: boolean }> = { id: 'alert-123', alertTypeId: '123', schedule: { interval: '10s' }, diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts index f540f9a9b884c..a020eecd448a4 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts @@ -9,7 +9,7 @@ import { IEvent } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; export interface AlertInstanceSummaryFromEventLogParams { - alert: SanitizedAlert; + alert: SanitizedAlert<{ bar: boolean }>; events: IEvent[]; dateStart: string; dateEnd: string; diff --git a/x-pack/plugins/alerts/server/lib/license_state.test.ts b/x-pack/plugins/alerts/server/lib/license_state.test.ts index 94db4c946ab00..2bba0a910b65e 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.test.ts @@ -56,7 +56,7 @@ describe('getLicenseCheckForAlertType', () => { let license: Subject; let licenseState: ILicenseState; const mockNotifyUsage = jest.fn(); - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -190,7 +190,7 @@ describe('ensureLicenseForAlertType()', () => { let license: Subject; let licenseState: ILicenseState; const mockNotifyUsage = jest.fn(); - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ diff --git a/x-pack/plugins/alerts/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts index dea5b3338a5be..e20ccea7c834f 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -13,7 +13,13 @@ import { LicensingPluginStart } from '../../../licensing/server'; import { ILicense, LicenseType } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; import { getAlertTypeFeatureUsageName } from './get_alert_type_feature_usage_name'; -import { AlertType } from '../types'; +import { + AlertType, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; import { AlertTypeDisabledError } from './errors/alert_type_disabled'; export type ILicenseState = PublicMethodsOf; @@ -130,7 +136,23 @@ export class LicenseState { } } - public ensureLicenseForAlertType(alertType: AlertType) { + public ensureLicenseForAlertType< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { this.notifyUsage(alertType.name, alertType.minimumLicenseRequired); const check = this.getLicenseCheckForAlertType( diff --git a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts index 2814eaef3e02a..634b6885aa59b 100644 --- a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts @@ -8,51 +8,19 @@ import { schema } from '@kbn/config-schema'; import { validateAlertTypeParams } from './validate_alert_type_params'; test('should return passed in params when validation not defined', () => { - const result = validateAlertTypeParams( - { - id: 'my-alert-type', - name: 'My description', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - async executor() {}, - producer: 'alerts', - }, - { - foo: true, - } - ); + const result = validateAlertTypeParams({ + foo: true, + }); expect(result).toEqual({ foo: true }); }); test('should validate and apply defaults when params is valid', () => { const result = validateAlertTypeParams( - { - id: 'my-alert-type', - name: 'My description', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - validate: { - params: schema.object({ - param1: schema.string(), - param2: schema.string({ defaultValue: 'default-value' }), - }), - }, - async executor() {}, - producer: 'alerts', - }, - { param1: 'value' } + { param1: 'value' }, + schema.object({ + param1: schema.string(), + param2: schema.string({ defaultValue: 'default-value' }), + }) ); expect(result).toEqual({ param1: 'value', @@ -63,26 +31,10 @@ test('should validate and apply defaults when params is valid', () => { test('should validate and throw error when params is invalid', () => { expect(() => validateAlertTypeParams( - { - id: 'my-alert-type', - name: 'My description', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - validate: { - params: schema.object({ - param1: schema.string(), - }), - }, - async executor() {}, - producer: 'alerts', - }, - {} + {}, + schema.object({ + param1: schema.string(), + }) ) ).toThrowErrorMatchingInlineSnapshot( `"params invalid: [param1]: expected value of type [string] but got [undefined]"` diff --git a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts index a443143d8cbde..2f510f90a2367 100644 --- a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts +++ b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts @@ -5,15 +5,14 @@ */ import Boom from '@hapi/boom'; -import { AlertType, AlertExecutorOptions } from '../types'; +import { AlertTypeParams, AlertTypeParamsValidator } from '../types'; -export function validateAlertTypeParams( - alertType: AlertType, - params: Record -): AlertExecutorOptions['params'] { - const validator = alertType.validate && alertType.validate.params; +export function validateAlertTypeParams( + params: Record, + validator?: AlertTypeParamsValidator +): Params { if (!validator) { - return params as AlertExecutorOptions['params']; + return params as Params; } try { diff --git a/x-pack/plugins/alerts/server/mocks.ts b/x-pack/plugins/alerts/server/mocks.ts index cfae4c650bd42..0f042b7a81d6c 100644 --- a/x-pack/plugins/alerts/server/mocks.ts +++ b/x-pack/plugins/alerts/server/mocks.ts @@ -11,6 +11,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../src/core/server/mocks'; +import { AlertInstanceContext, AlertInstanceState } from './types'; export { alertsClientMock }; @@ -30,8 +31,14 @@ const createStartMock = () => { return mock; }; -export type AlertInstanceMock = jest.Mocked; -const createAlertInstanceFactoryMock = () => { +export type AlertInstanceMock< + State extends AlertInstanceState = AlertInstanceState, + Context extends AlertInstanceContext = AlertInstanceContext +> = jest.Mocked>; +const createAlertInstanceFactoryMock = < + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext +>() => { const mock = { hasScheduledActions: jest.fn(), isThrottled: jest.fn(), @@ -50,14 +57,17 @@ const createAlertInstanceFactoryMock = () => { mock.unscheduleActions.mockReturnValue(mock); mock.scheduleActions.mockReturnValue(mock); - return (mock as unknown) as AlertInstanceMock; + return (mock as unknown) as AlertInstanceMock; }; -const createAlertServicesMock = () => { - const alertInstanceFactoryMock = createAlertInstanceFactoryMock(); +const createAlertServicesMock = < + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext +>() => { + const alertInstanceFactoryMock = createAlertInstanceFactoryMock(); return { alertInstanceFactory: jest - .fn, [string]>() + .fn>, [string]>() .mockReturnValue(alertInstanceFactoryMock), callCluster: elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser, getLegacyScopedClusterClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 6288d27c6ebe0..ece6fa2328d68 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -58,7 +58,7 @@ describe('Alerting Plugin', () => { describe('registerType()', () => { let setup: PluginSetupContract; - const sampleAlertType: AlertType = { + const sampleAlertType: AlertType = { id: 'test', name: 'test', minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 63861f5050f25..d15ae0ca55ef9 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -102,9 +102,18 @@ export interface PluginSetupContract { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never >( - alertType: AlertType + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > ): void; } @@ -273,8 +282,19 @@ export class AlertingPlugin { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext - >(alertType: AlertType) { + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { if (!(alertType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${alertType.minimumLicenseRequired}" is not a valid license type`); } diff --git a/x-pack/plugins/alerts/server/routes/create.test.ts b/x-pack/plugins/alerts/server/routes/create.test.ts index 5597b315158cd..fc531821f25b6 100644 --- a/x-pack/plugins/alerts/server/routes/create.test.ts +++ b/x-pack/plugins/alerts/server/routes/create.test.ts @@ -49,7 +49,7 @@ describe('createAlertRoute', () => { ], }; - const createResult: Alert = { + const createResult: Alert<{ bar: boolean }> = { ...mockedAlert, enabled: true, muteAll: false, diff --git a/x-pack/plugins/alerts/server/routes/create.ts b/x-pack/plugins/alerts/server/routes/create.ts index a34a3118985fa..a79a9d40b236f 100644 --- a/x-pack/plugins/alerts/server/routes/create.ts +++ b/x-pack/plugins/alerts/server/routes/create.ts @@ -16,7 +16,13 @@ import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { validateDurationSchema } from '../lib'; import { handleDisabledApiKeysError } from './lib/error_handler'; -import { Alert, AlertNotifyWhenType, BASE_ALERT_API_PATH, validateNotifyWhenType } from '../types'; +import { + Alert, + AlertNotifyWhenType, + AlertTypeParams, + BASE_ALERT_API_PATH, + validateNotifyWhenType, +} from '../types'; import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; export const bodySchema = schema.object({ @@ -65,7 +71,9 @@ export const createAlertRoute = (router: IRouter, licenseState: ILicenseState) = const alert = req.body; const notifyWhen = alert?.notifyWhen ? (alert.notifyWhen as AlertNotifyWhenType) : null; try { - const alertRes: Alert = await alertsClient.create({ data: { ...alert, notifyWhen } }); + const alertRes: Alert = await alertsClient.create({ + data: { ...alert, notifyWhen }, + }); return res.ok({ body: alertRes, }); diff --git a/x-pack/plugins/alerts/server/routes/get.test.ts b/x-pack/plugins/alerts/server/routes/get.test.ts index 21e52ece82d2d..747f9b11e2b47 100644 --- a/x-pack/plugins/alerts/server/routes/get.test.ts +++ b/x-pack/plugins/alerts/server/routes/get.test.ts @@ -22,7 +22,9 @@ beforeEach(() => { }); describe('getAlertRoute', () => { - const mockedAlert: Alert = { + const mockedAlert: Alert<{ + bar: true; + }> = { id: '1', alertTypeId: '1', schedule: { interval: '10s' }, diff --git a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts index 09236ec5e0ad1..1bd8b75e2133d 100644 --- a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts @@ -9,7 +9,9 @@ import { AlertTaskInstance, taskInstanceToAlertTaskInstance } from './alert_task import uuid from 'uuid'; import { SanitizedAlert } from '../types'; -const alert: SanitizedAlert = { +const alert: SanitizedAlert<{ + bar: boolean; +}> = { id: 'alert-123', alertTypeId: '123', schedule: { interval: '10s' }, diff --git a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts index a290f3fa33c70..ab074cfdffa1c 100644 --- a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts +++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts @@ -13,6 +13,7 @@ import { alertParamsSchema, alertStateSchema, AlertTaskParams, + AlertTypeParams, } from '../../common'; export interface AlertTaskInstance extends ConcreteTaskInstance { @@ -23,9 +24,9 @@ export interface AlertTaskInstance extends ConcreteTaskInstance { const enumerateErrorFields = (e: t.Errors) => `${e.map(({ context }) => context.map(({ key }) => key).join('.'))}`; -export function taskInstanceToAlertTaskInstance( +export function taskInstanceToAlertTaskInstance( taskInstance: ConcreteTaskInstance, - alert?: SanitizedAlert + alert?: SanitizedAlert ): AlertTaskInstance { return { ...taskInstance, diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index b414e726f0101..5ab44a6ccdb51 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertType } from '../types'; -import { createExecutionHandler } from './create_execution_handler'; +import { createExecutionHandler, CreateExecutionHandlerOptions } from './create_execution_handler'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { actionsMock, @@ -16,12 +15,26 @@ import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { KibanaRequest } from 'kibana/server'; import { asSavedObjectExecutionSource } from '../../../actions/server'; import { InjectActionParamsOpts } from './inject_action_params'; +import { NormalizedAlertType } from '../alert_type_registry'; +import { + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; jest.mock('./inject_action_params', () => ({ injectActionParams: jest.fn(), })); -const alertType: AlertType = { +const alertType: NormalizedAlertType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + 'default' | 'other-group', + 'recovered' +> = { id: 'test', name: 'Test', actionGroups: [ @@ -39,18 +52,28 @@ const alertType: AlertType = { }; const actionsClient = actionsClientMock.create(); -const createExecutionHandlerParams = { - actionsPlugin: actionsMock.createStart(), + +const mockActionsPlugin = actionsMock.createStart(); +const mockEventLogger = eventLoggerMock.create(); +const createExecutionHandlerParams: jest.Mocked< + CreateExecutionHandlerOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + 'default' | 'other-group', + 'recovered' + > +> = { + actionsPlugin: mockActionsPlugin, spaceId: 'default', alertId: '1', alertName: 'name-of-alert', tags: ['tag-A', 'tag-B'], apiKey: 'MTIzOmFiYw==', - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - getBasePath: jest.fn().mockReturnValue(undefined), alertType, logger: loggingSystemMock.create().get(), - eventLogger: eventLoggerMock.create(), + eventLogger: mockEventLogger, actions: [ { id: '1', @@ -79,12 +102,10 @@ beforeEach(() => { .injectActionParams.mockImplementation( ({ actionParams }: InjectActionParamsOpts) => actionParams ); - createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); - createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); - createExecutionHandlerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( - actionsClient - ); - createExecutionHandlerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation( + mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true); + mockActionsPlugin.isActionExecutable.mockReturnValue(true); + mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient); + mockActionsPlugin.renderActionParameterTemplates.mockImplementation( renderActionParameterTemplatesDefault ); }); @@ -97,9 +118,9 @@ test('enqueues execution per selected action', async () => { context: {}, alertInstanceId: '2', }); - expect( - createExecutionHandlerParams.actionsPlugin.getActionsClientWithRequest - ).toHaveBeenCalledWith(createExecutionHandlerParams.request); + expect(mockActionsPlugin.getActionsClientWithRequest).toHaveBeenCalledWith( + createExecutionHandlerParams.request + ); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -124,9 +145,8 @@ test('enqueues execution per selected action', async () => { ] `); - const eventLogger = createExecutionHandlerParams.eventLogger; - expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + expect(mockEventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(mockEventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -171,9 +191,9 @@ test('enqueues execution per selected action', async () => { test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { // Mock two calls, one for check against actions[0] and the second for actions[1] - createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValueOnce(false); - createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); - createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); + mockActionsPlugin.isActionExecutable.mockReturnValueOnce(false); + mockActionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); + mockActionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); const executionHandler = createExecutionHandler({ ...createExecutionHandlerParams, actions: [ @@ -214,9 +234,9 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => }); test('trow error error message when action type is disabled', async () => { - createExecutionHandlerParams.actionsPlugin.preconfiguredActions = []; - createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(false); - createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(false); + mockActionsPlugin.preconfiguredActions = []; + mockActionsPlugin.isActionExecutable.mockReturnValue(false); + mockActionsPlugin.isActionTypeEnabled.mockReturnValue(false); const executionHandler = createExecutionHandler({ ...createExecutionHandlerParams, actions: [ @@ -243,7 +263,7 @@ test('trow error error message when action type is disabled', async () => { expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0); - createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockImplementation(() => true); + mockActionsPlugin.isActionExecutable.mockImplementation(() => true); const executionHandlerForPreconfiguredAction = createExecutionHandler({ ...createExecutionHandlerParams, actions: [...createExecutionHandlerParams.actions], @@ -337,7 +357,9 @@ test('state attribute gets parameterized', async () => { test(`logs an error when action group isn't part of actionGroups available for the alertType`, async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); const result = await executionHandler({ - actionGroup: 'invalid-group', + // we have to trick the compiler as this is an invalid type and this test checks whether we + // enforce this at runtime as well as compile time + actionGroup: 'invalid-group' as 'default' | 'other-group', context: {}, state: {}, alertInstanceId: '2', diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 8c7ad79483194..c3d90c7bcf08b 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -15,14 +15,22 @@ import { EVENT_LOG_ACTIONS } from '../plugin'; import { injectActionParams } from './inject_action_params'; import { AlertAction, + AlertTypeParams, + AlertTypeState, AlertInstanceState, AlertInstanceContext, - AlertType, - AlertTypeParams, RawAlert, } from '../types'; +import { NormalizedAlertType } from '../alert_type_registry'; -interface CreateExecutionHandlerOptions { +export interface CreateExecutionHandlerOptions< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> { alertId: string; alertName: string; tags?: string[]; @@ -30,22 +38,40 @@ interface CreateExecutionHandlerOptions { actions: AlertAction[]; spaceId: string; apiKey: RawAlert['apiKey']; - alertType: AlertType; + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >; logger: Logger; eventLogger: IEventLogger; request: KibanaRequest; alertParams: AlertTypeParams; } -interface ExecutionHandlerOptions { - actionGroup: string; +interface ExecutionHandlerOptions { + actionGroup: ActionGroupIds; actionSubgroup?: string; alertInstanceId: string; context: AlertInstanceContext; state: AlertInstanceState; } -export function createExecutionHandler({ +export type ExecutionHandler = ( + options: ExecutionHandlerOptions +) => Promise; + +export function createExecutionHandler< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +>({ logger, alertId, alertName, @@ -58,7 +84,14 @@ export function createExecutionHandler({ eventLogger, request, alertParams, -}: CreateExecutionHandlerOptions) { +}: CreateExecutionHandlerOptions< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId +>): ExecutionHandler { const alertTypeActionGroups = new Map( alertType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) ); @@ -68,7 +101,7 @@ export function createExecutionHandler({ context, state, alertInstanceId, - }: ExecutionHandlerOptions) => { + }: ExecutionHandlerOptions) => { if (!alertTypeActionGroups.has(actionGroup)) { logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`); return; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index a4b565194e431..75be9d371aee4 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -6,7 +6,13 @@ import sinon from 'sinon'; import { schema } from '@kbn/config-schema'; -import { AlertExecutorOptions } from '../types'; +import { + AlertExecutorOptions, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; import { ConcreteTaskInstance, isUnrecoverableError, @@ -28,9 +34,9 @@ import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; -import { NormalizedAlertType } from '../alert_type_registry'; +import { UntypedNormalizedAlertType } from '../alert_type_registry'; import { alertTypeRegistryMock } from '../alert_type_registry.mock'; -const alertType = { +const alertType: jest.Mocked = { id: 'test', name: 'My test alert', actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], @@ -91,7 +97,7 @@ describe('Task Runner', () => { alertTypeRegistry, }; - const mockedAlertTypeSavedObject: Alert = { + const mockedAlertTypeSavedObject: Alert = { id: '1', consumer: 'bar', createdAt: new Date('2019-02-12T21:01:22.479Z'), @@ -150,7 +156,7 @@ describe('Task Runner', () => { test('successfully executes the task', async () => { const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, { ...mockedTaskInstance, state: { @@ -254,14 +260,22 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices .alertInstanceFactory('1') .scheduleActionsWithSubGroup('default', 'subDefault'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -407,12 +421,20 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -516,13 +538,21 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); executorServices.alertInstanceFactory('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -562,12 +592,20 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, { ...mockedTaskInstance, state: { @@ -656,12 +694,20 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, { ...mockedTaskInstance, state: { @@ -696,14 +742,22 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices .alertInstanceFactory('1') .scheduleActionsWithSubGroup('default', 'subgroup1'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, { ...mockedTaskInstance, state: { @@ -744,12 +798,20 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -912,12 +974,20 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, { ...mockedTaskInstance, state: { @@ -1012,12 +1082,20 @@ describe('Task Runner', () => { }; alertTypeWithCustomRecovery.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( - alertTypeWithCustomRecovery as NormalizedAlertType, + alertTypeWithCustomRecovery, { ...mockedTaskInstance, state: { @@ -1103,13 +1181,21 @@ describe('Task Runner', () => { test('persists alertInstances passed in from state, only if they are scheduled for execution', async () => { alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); const date = new Date().toISOString(); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, { ...mockedTaskInstance, state: { @@ -1239,7 +1325,7 @@ describe('Task Runner', () => { param1: schema.string(), }), }, - } as NormalizedAlertType, + }, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1267,7 +1353,7 @@ describe('Task Runner', () => { test('uses API key when provided', async () => { const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1300,7 +1386,7 @@ describe('Task Runner', () => { test(`doesn't use API key when not provided`, async () => { const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1330,7 +1416,7 @@ describe('Task Runner', () => { test('rescheduled the Alert if the schedule has update during a task run', async () => { const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1365,13 +1451,21 @@ describe('Task Runner', () => { test('recovers gracefully when the AlertType executor throws an exception', async () => { alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { throw new Error('OMG'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1438,7 +1532,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1497,7 +1591,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1564,7 +1658,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1631,7 +1725,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1701,7 +1795,7 @@ describe('Task Runner', () => { const legacyTaskInstance = omit(mockedTaskInstance, 'schedule'); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, legacyTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1733,13 +1827,21 @@ describe('Task Runner', () => { }; alertType.executor.mockImplementation( - ({ services: executorServices }: AlertExecutorOptions) => { + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { throw new Error('OMG'); } ); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, { ...mockedTaskInstance, state: originalAlertSate, @@ -1770,7 +1872,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType as NormalizedAlertType, + alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 44cf7dd91be7d..12f7c33ae5052 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -10,7 +10,7 @@ import { addSpaceIdToPath } from '../../../spaces/server'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; -import { createExecutionHandler } from './create_execution_handler'; +import { createExecutionHandler, ExecutionHandler } from './create_execution_handler'; import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; import { validateAlertTypeParams, @@ -26,7 +26,6 @@ import { RawAlertInstance, AlertTaskState, Alert, - AlertExecutorOptions, SanitizedAlert, AlertExecutionStatus, AlertExecutionStatusErrorReasons, @@ -39,7 +38,14 @@ import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_l import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; import { AlertsClient } from '../alerts_client'; import { partiallyUpdateAlert } from '../saved_objects'; -import { ActionGroup } from '../../common'; +import { + ActionGroup, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + WithoutReservedActionGroups, +} from '../../common'; import { NormalizedAlertType } from '../alert_type_registry'; const FALLBACK_RETRY_INTERVAL = '5m'; @@ -55,15 +61,36 @@ interface AlertTaskInstance extends ConcreteTaskInstance { state: AlertTaskState; } -export class TaskRunner { +export class TaskRunner< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> { private context: TaskRunnerContext; private logger: Logger; private taskInstance: AlertTaskInstance; - private alertType: NormalizedAlertType; + private alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >; private readonly alertTypeRegistry: AlertTypeRegistry; constructor( - alertType: NormalizedAlertType, + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >, taskInstance: ConcreteTaskInstance, context: TaskRunnerContext ) { @@ -131,10 +158,17 @@ export class TaskRunner { tags: string[] | undefined, spaceId: string, apiKey: RawAlert['apiKey'], - actions: Alert['actions'], - alertParams: RawAlert['params'] + actions: Alert['actions'], + alertParams: Params ) { - return createExecutionHandler({ + return createExecutionHandler< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >({ alertId, alertName, tags, @@ -152,8 +186,8 @@ export class TaskRunner { async executeAlertInstance( alertInstanceId: string, - alertInstance: AlertInstance, - executionHandler: ReturnType + alertInstance: AlertInstance, + executionHandler: ExecutionHandler ) { const { actionGroup, @@ -168,9 +202,9 @@ export class TaskRunner { async executeAlertInstances( services: Services, - alert: SanitizedAlert, - params: AlertExecutorOptions['params'], - executionHandler: ReturnType, + alert: SanitizedAlert, + params: Params, + executionHandler: ExecutionHandler, spaceId: string, event: Event ): Promise { @@ -190,9 +224,12 @@ export class TaskRunner { } = this.taskInstance; const namespace = this.context.spaceIdToNamespace(spaceId); - const alertInstances = mapValues, AlertInstance>( + const alertInstances = mapValues< + Record, + AlertInstance + >( alertRawInstances, - (rawAlertInstance) => new AlertInstance(rawAlertInstance) + (rawAlertInstance) => new AlertInstance(rawAlertInstance) ); const originalAlertInstances = cloneDeep(alertInstances); @@ -205,10 +242,14 @@ export class TaskRunner { alertId, services: { ...services, - alertInstanceFactory: createAlertInstanceFactory(alertInstances), + alertInstanceFactory: createAlertInstanceFactory< + InstanceState, + InstanceContext, + WithoutReservedActionGroups + >(alertInstances), }, params, - state: alertTypeState, + state: alertTypeState as State, startedAt: this.taskInstance.startedAt!, previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null, spaceId, @@ -232,12 +273,15 @@ export class TaskRunner { event.event.outcome = 'success'; // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object - const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => - alertInstance.hasScheduledActions() + const instancesWithScheduledActions = pickBy( + alertInstances, + (alertInstance: AlertInstance) => + alertInstance.hasScheduledActions() ); const recoveredAlertInstances = pickBy( alertInstances, - (alertInstance: AlertInstance) => !alertInstance.hasScheduledActions() + (alertInstance: AlertInstance) => + !alertInstance.hasScheduledActions() ); logActiveAndRecoveredInstances({ @@ -260,7 +304,7 @@ export class TaskRunner { if (!muteAll) { const mutedInstanceIdsSet = new Set(mutedInstanceIds); - scheduleActionsForRecoveredInstances({ + scheduleActionsForRecoveredInstances({ recoveryActionGroup: this.alertType.recoveryActionGroup, recoveredAlertInstances, executionHandler, @@ -272,7 +316,10 @@ export class TaskRunner { const instancesToExecute = notifyWhen === 'onActionGroupChange' ? Object.entries(instancesWithScheduledActions).filter( - ([alertInstanceName, alertInstance]: [string, AlertInstance]) => { + ([alertInstanceName, alertInstance]: [ + string, + AlertInstance + ]) => { const shouldExecuteAction = alertInstance.scheduledActionGroupOrSubgroupHasChanged(); if (!shouldExecuteAction) { this.logger.debug( @@ -283,7 +330,10 @@ export class TaskRunner { } ) : Object.entries(instancesWithScheduledActions).filter( - ([alertInstanceName, alertInstance]: [string, AlertInstance]) => { + ([alertInstanceName, alertInstance]: [ + string, + AlertInstance + ]) => { const throttled = alertInstance.isThrottled(throttle); const muted = mutedInstanceIdsSet.has(alertInstanceName); const shouldExecuteAction = !throttled && !muted; @@ -299,8 +349,9 @@ export class TaskRunner { ); await Promise.all( - instancesToExecute.map(([id, alertInstance]: [string, AlertInstance]) => - this.executeAlertInstance(id, alertInstance, executionHandler) + instancesToExecute.map( + ([id, alertInstance]: [string, AlertInstance]) => + this.executeAlertInstance(id, alertInstance, executionHandler) ) ); } else { @@ -309,17 +360,17 @@ export class TaskRunner { return { alertTypeState: updatedAlertTypeState || undefined, - alertInstances: mapValues, RawAlertInstance>( - instancesWithScheduledActions, - (alertInstance) => alertInstance.toRaw() - ), + alertInstances: mapValues< + Record>, + RawAlertInstance + >(instancesWithScheduledActions, (alertInstance) => alertInstance.toRaw()), }; } async validateAndExecuteAlert( services: Services, apiKey: RawAlert['apiKey'], - alert: SanitizedAlert, + alert: SanitizedAlert, event: Event ) { const { @@ -327,7 +378,7 @@ export class TaskRunner { } = this.taskInstance; // Validate - const validatedParams = validateAlertTypeParams(this.alertType, alert.params); + const validatedParams = validateAlertTypeParams(alert.params, this.alertType.validate?.params); const executionHandler = this.getExecutionHandler( alertId, alert.name, @@ -359,7 +410,7 @@ export class TaskRunner { } const [services, alertsClient] = this.getServicesWithSpaceLevelPermissions(spaceId, apiKey); - let alert: SanitizedAlert; + let alert: SanitizedAlert; // Ensure API key is still valid and user has access try { @@ -501,19 +552,23 @@ export class TaskRunner { } } -interface GenerateNewAndRecoveredInstanceEventsParams { +interface GenerateNewAndRecoveredInstanceEventsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +> { eventLogger: IEventLogger; - originalAlertInstances: Dictionary; - currentAlertInstances: Dictionary; - recoveredAlertInstances: Dictionary; + originalAlertInstances: Dictionary>; + currentAlertInstances: Dictionary>; + recoveredAlertInstances: Dictionary>; alertId: string; alertLabel: string; namespace: string | undefined; } -function generateNewAndRecoveredInstanceEvents( - params: GenerateNewAndRecoveredInstanceEventsParams -) { +function generateNewAndRecoveredInstanceEvents< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +>(params: GenerateNewAndRecoveredInstanceEventsParams) { const { eventLogger, alertId, @@ -584,16 +639,32 @@ function generateNewAndRecoveredInstanceEvents( } } -interface ScheduleActionsForRecoveredInstancesParams { +interface ScheduleActionsForRecoveredInstancesParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string +> { logger: Logger; - recoveryActionGroup: ActionGroup; - recoveredAlertInstances: Dictionary; - executionHandler: ReturnType; + recoveryActionGroup: ActionGroup; + recoveredAlertInstances: Dictionary< + AlertInstance + >; + executionHandler: ExecutionHandler; mutedInstanceIdsSet: Set; alertLabel: string; } -function scheduleActionsForRecoveredInstances(params: ScheduleActionsForRecoveredInstancesParams) { +function scheduleActionsForRecoveredInstances< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string +>( + params: ScheduleActionsForRecoveredInstancesParams< + InstanceState, + InstanceContext, + RecoveryActionGroupId + > +) { const { logger, recoveryActionGroup, @@ -623,14 +694,33 @@ function scheduleActionsForRecoveredInstances(params: ScheduleActionsForRecovere } } -interface LogActiveAndRecoveredInstancesParams { +interface LogActiveAndRecoveredInstancesParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> { logger: Logger; - activeAlertInstances: Dictionary; - recoveredAlertInstances: Dictionary; + activeAlertInstances: Dictionary>; + recoveredAlertInstances: Dictionary< + AlertInstance + >; alertLabel: string; } -function logActiveAndRecoveredInstances(params: LogActiveAndRecoveredInstancesParams) { +function logActiveAndRecoveredInstances< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +>( + params: LogActiveAndRecoveredInstancesParams< + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > +) { const { logger, activeAlertInstances, recoveredAlertInstances, alertLabel } = params; const activeInstanceIds = Object.keys(activeAlertInstances); const recoveredInstanceIds = Object.keys(recoveredAlertInstances); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index 6c58b64fffa92..3a5a130f582ed 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -16,10 +16,10 @@ import { import { actionsMock } from '../../../actions/server/mocks'; import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; -import { NormalizedAlertType } from '../alert_type_registry'; +import { UntypedNormalizedAlertType } from '../alert_type_registry'; import { alertTypeRegistryMock } from '../alert_type_registry.mock'; -const alertType: NormalizedAlertType = { +const alertType: UntypedNormalizedAlertType = { id: 'test', name: 'My test alert', actionGroups: [{ id: 'default', name: 'Default' }], diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index 1fe94972bd4b0..2d57467075987 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -13,7 +13,15 @@ import { import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; -import { AlertTypeRegistry, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types'; +import { + AlertTypeParams, + AlertTypeRegistry, + GetServicesFunction, + SpaceIdToNamespaceFunction, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; import { TaskRunner } from './task_runner'; import { IEventLogger } from '../../../event_log/server'; import { AlertsClient } from '../alerts_client'; @@ -44,11 +52,35 @@ export class TaskRunnerFactory { this.taskRunnerContext = taskRunnerContext; } - public create(alertType: NormalizedAlertType, { taskInstance }: RunContext) { + public create< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >, + { taskInstance }: RunContext + ) { if (!this.isInitialized) { throw new Error('TaskRunnerFactory not initialized'); } - return new TaskRunner(alertType, taskInstance, this.taskRunnerContext!); + return new TaskRunner< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >(alertType, taskInstance, this.taskRunnerContext!); } } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 8704068c3e51a..39c52d9653aaa 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -29,6 +29,7 @@ import { AlertExecutionStatusErrorReasons, AlertsHealth, AlertNotifyWhenType, + WithoutReservedActionGroups, } from '../common'; import { LicenseType } from '../../licensing/server'; @@ -47,6 +48,9 @@ declare module 'src/core/server' { } export interface Services { + /** + * @deprecated Use `scopedClusterClient` instead. + */ callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; scopedClusterClient: ElasticsearchClient; @@ -55,21 +59,25 @@ export interface Services { export interface AlertServices< InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never > extends Services { - alertInstanceFactory: (id: string) => PublicAlertInstance; + alertInstanceFactory: ( + id: string + ) => PublicAlertInstance; } export interface AlertExecutorOptions< - Params extends AlertTypeParams = AlertTypeParams, - State extends AlertTypeState = AlertTypeState, - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never > { alertId: string; startedAt: Date; previousStartedAt: Date | null; - services: AlertServices; + services: AlertServices; params: Params; state: State; spaceId: string; @@ -85,31 +93,46 @@ export interface ActionVariable { description: string; } -// signature of the alert type executor function export type ExecutorType< - Params, - State, - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never > = ( - options: AlertExecutorOptions + options: AlertExecutorOptions ) => Promise; +export interface AlertTypeParamsValidator { + validate: (object: unknown) => Params; +} export interface AlertType< - Params extends AlertTypeParams = AlertTypeParams, - State extends AlertTypeState = AlertTypeState, - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never > { id: string; name: string; validate?: { - params?: { validate: (object: unknown) => Params }; + params?: AlertTypeParamsValidator; }; - actionGroups: ActionGroup[]; - defaultActionGroupId: ActionGroup['id']; - recoveryActionGroup?: ActionGroup; - executor: ExecutorType; + actionGroups: Array>; + defaultActionGroupId: ActionGroup['id']; + recoveryActionGroup?: ActionGroup; + executor: ExecutorType< + Params, + State, + InstanceState, + InstanceContext, + /** + * Ensure that the reserved ActionGroups (such as `Recovered`) are not + * available for scheduling in the Executor + */ + WithoutReservedActionGroups + >; producer: string; actionVariables?: { context?: ActionVariable[]; @@ -119,6 +142,13 @@ export interface AlertType< minimumLicenseRequired: LicenseType; } +export type UntypedAlertType = AlertType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext +>; + export interface RawAlertAction extends SavedObjectAttributes { group: string; actionRef: string; @@ -142,7 +172,8 @@ export interface RawAlertExecutionStatus extends SavedObjectAttributes { }; } -export type PartialAlert = Pick & Partial>; +export type PartialAlert = Pick, 'id'> & + Partial, 'id'>>; export interface RawAlert extends SavedObjectAttributes { enabled: boolean; diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 7cc36253ef581..bb42c8acd167a 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { ValuesType } from 'utility-types'; +import { ActionGroup } from '../../alerts/common'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../../ml/common'; export enum AlertType { @@ -15,20 +16,31 @@ export enum AlertType { TransactionDurationAnomaly = 'apm.transaction_duration_anomaly', } -const THRESHOLD_MET_GROUP = { - id: 'threshold_met', +export const THRESHOLD_MET_GROUP_ID = 'threshold_met'; +export type ThresholdMetActionGroupId = typeof THRESHOLD_MET_GROUP_ID; +const THRESHOLD_MET_GROUP: ActionGroup = { + id: THRESHOLD_MET_GROUP_ID, name: i18n.translate('xpack.apm.a.thresholdMet', { defaultMessage: 'Threshold met', }), }; -export const ALERT_TYPES_CONFIG = { +export const ALERT_TYPES_CONFIG: Record< + AlertType, + { + name: string; + actionGroups: Array>; + defaultActionGroupId: ThresholdMetActionGroupId; + minimumLicenseRequired: string; + producer: string; + } +> = { [AlertType.ErrorCount]: { name: i18n.translate('xpack.apm.errorCountAlert.name', { defaultMessage: 'Error count threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -37,7 +49,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction duration threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -46,7 +58,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction duration anomaly', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -55,7 +67,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction error rate threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, diff --git a/x-pack/plugins/apm/public/application/action_menu/index.tsx b/x-pack/plugins/apm/public/application/action_menu/index.tsx index 438eb2bca7f24..32359e722e001 100644 --- a/x-pack/plugins/apm/public/application/action_menu/index.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/index.tsx @@ -28,6 +28,7 @@ export function ActionMenu() { canSaveAlerts, canReadAnomalies, } = getAlertingCapabilities(plugins, capabilities); + const canSaveApmAlerts = capabilities.apm.save && canSaveAlerts; function apmHref(path: string) { return getAPMHref({ basePath, path, search }); @@ -52,7 +53,7 @@ export function ActionMenu() { diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index 88a897d7baf50..83bbf49691be9 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { AlertType } from '../../../../common/alert_types'; +import { getInitialAlertValues } from '../get_initial_alert_values'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public'; - interface Props { addFlyoutVisible: boolean; setAddFlyoutVisibility: React.Dispatch>; @@ -20,10 +21,13 @@ interface KibanaDeps { export function AlertingFlyout(props: Props) { const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; + const { serviceName } = useParams<{ serviceName?: string }>(); const { services: { triggersActionsUi }, } = useKibana(); + const initialValues = getInitialAlertValues(alertType, serviceName); + const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [ setAddFlyoutVisibility, ]); @@ -36,7 +40,9 @@ export function AlertingFlyout(props: Props) { onClose: onCloseAddFlyout, alertTypeId: alertType, canChangeTrigger: false, + initialValues, }), + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertType, onCloseAddFlyout, triggersActionsUi] ); return <>{addFlyoutVisible && addAlertFlyout}; diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx index 1ed5748cd757e..f01f5d21aef40 100644 --- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx @@ -46,10 +46,10 @@ export function ChartPreview({ const yMax = Math.max(...values, threshold * 1.2); const style = { - fill: theme.eui.euiColorVis9, + fill: theme.eui.euiColorVis2, line: { strokeWidth: 2, - stroke: theme.eui.euiColorVis9, + stroke: theme.eui.euiColorVis2, opacity: 1, }, opacity: thresholdOpacity, diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index cce973f8587da..d7375d14e17cf 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asInteger } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -110,7 +109,6 @@ export function ErrorCountAlertTrigger(props: Props) { return ( { + expect(getInitialAlertValues(null, undefined)).toEqual({ tags: ['apm'] }); +}); + +test('handles valid alert type', () => { + const alertType = AlertType.ErrorCount; + expect(getInitialAlertValues(alertType, undefined)).toEqual({ + name: ALERT_TYPES_CONFIG[alertType].name, + tags: ['apm'], + }); + + expect(getInitialAlertValues(alertType, 'Service Name')).toEqual({ + name: `${ALERT_TYPES_CONFIG[alertType].name} | Service Name`, + tags: ['apm', `service.name:service name`], + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/get_initial_alert_values.ts b/x-pack/plugins/apm/public/components/alerting/get_initial_alert_values.ts new file mode 100644 index 0000000000000..3655c9f90c4bf --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/get_initial_alert_values.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; + +export function getInitialAlertValues( + alertType: AlertType | null, + serviceName: string | undefined +) { + const alertTypeName = alertType + ? ALERT_TYPES_CONFIG[alertType].name + : undefined; + const alertName = alertTypeName + ? serviceName + ? `${alertTypeName} | ${serviceName}` + : alertTypeName + : undefined; + const tags = ['apm']; + if (serviceName) { + tags.push(`service.name:${serviceName}`.toLowerCase()); + } + + return { + tags, + ...(alertName ? { name: alertName } : {}), + }; +} diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx index 0a12f79bf61a9..a04679bcb689d 100644 --- a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx @@ -9,7 +9,6 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; interface Props { - alertTypeName: string; setAlertParams: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; defaults: Record; @@ -20,14 +19,7 @@ interface Props { export function ServiceAlertTrigger(props: Props) { const { serviceName } = useParams<{ serviceName?: string }>(); - const { - fields, - setAlertParams, - setAlertProperty, - alertTypeName, - defaults, - chartPreview, - } = props; + const { fields, setAlertParams, defaults, chartPreview } = props; const params: Record = { ...defaults, @@ -36,17 +28,6 @@ export function ServiceAlertTrigger(props: Props) { useEffect(() => { // we only want to run this on mount to set default values - - const alertName = params.serviceName - ? `${alertTypeName} | ${params.serviceName}` - : alertTypeName; - setAlertProperty('name', alertName); - - const tags = ['apm']; - if (params.serviceName) { - tags.push(`service.name:${params.serviceName}`.toLowerCase()); - } - setAlertProperty('tags', tags); Object.keys(params).forEach((key) => { setAlertParams(key, params[key]); }); diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx index 72611043bbed3..7f9a27e884e8e 100644 --- a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx +++ b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx @@ -18,7 +18,6 @@ describe('ServiceAlertTrigger', () => { expect(() => render( {}} diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index 22840bc2e6ed0..7c0a74f2e1b60 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { useFetcher } from '../../../../../observability/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; import { TimeSeries } from '../../../../typings/timeseries'; @@ -203,7 +202,6 @@ export function TransactionDurationAlertTrigger(props: Props) { return ( { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.test.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.test.tsx index a492938deffab..c469a2c21c34a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.test.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; import { createMemoryHistory } from 'history'; -import * as fetcherHook from '../../../../../../hooks/use_fetcher'; -import { SelectableUrlList } from '../SelectableUrlList'; -import { render } from '../../../utils/test_helper'; +import * as fetcherHook from '../../../../../hooks/use_fetcher'; +import { SelectableUrlList } from './SelectableUrlList'; +import { render } from '../../utils/test_helper'; describe('SelectableUrlList', () => { it('it uses search term value from url', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/FormatToSec.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts similarity index 94% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/FormatToSec.test.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts index 6cdf469d980fa..764d662615031 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/FormatToSec.test.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { formatToSec } from '../KeyUXMetrics'; +import { formatToSec } from './KeyUXMetrics'; describe('FormatToSec', () => { test('it returns the expected value', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx index baa9cb7dd74f9..804eeaec26655 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import * as fetcherHook from '../../../../../hooks/use_fetcher'; -import { KeyUXMetrics } from '../KeyUXMetrics'; +import * as fetcherHook from '../../../../hooks/use_fetcher'; +import { KeyUXMetrics } from './KeyUXMetrics'; describe('KeyUXMetrics', () => { it('renders metrics with correct formats', () => { @@ -23,7 +23,7 @@ describe('KeyUXMetrics', () => { { test('it renders', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx index cbaae40b04361..89f20bf24ccba 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx @@ -7,7 +7,7 @@ import { render, shallow } from 'enzyme'; import React from 'react'; -import { MapToolTip } from '../MapToolTip'; +import { MapToolTip } from './MapToolTip'; describe('Map Tooltip', () => { test('it shallow renders', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/EmbeddedMap.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/EmbeddedMap.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/MapToolTip.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/MapToolTip.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts similarity index 92% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts index 872553452b263..a63ab11263e5f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts @@ -6,7 +6,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { mockLayerList } from './__mocks__/regions_layer.mock'; -import { useLayerList } from '../useLayerList'; +import { useLayerList } from './useLayerList'; describe('useLayerList', () => { test('it returns the region layer', () => { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index 2f05842b6bdec..e7ce4bb24b38f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -5,6 +5,7 @@ */ import { render } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; import { CoreStart } from 'kibana/public'; import React, { ReactNode } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; @@ -15,6 +16,10 @@ import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_ap import { LicenseContext } from '../../../context/license/license_context'; import * as useFetcherModule from '../../../hooks/use_fetcher'; import { ServiceMap } from './'; +import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context'; +import { Router } from 'react-router-dom'; + +const history = createMemoryHistory(); const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -49,7 +54,9 @@ function createWrapper(license: License | null) { - {children} + + {children} + diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 48a7f8f77ab84..da4a8596970ec 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -39,21 +39,34 @@ const ServiceMapDatePickerFlexGroup = styled(EuiFlexGroup)` margin: 0; `; +function DatePickerSection() { + return ( + + + + + + ); +} + function PromptContainer({ children }: { children: ReactNode }) { return ( - - + + - {children} - - + + {children} + + + ); } @@ -137,11 +150,7 @@ export function ServiceMap({ return ( <> - - - - - +
), }, - { - width: px(units.double), - name: '', - render: (config: Config) => ( - - ), - }, - { - width: px(units.double), - name: '', - render: (config: Config) => ( - setConfigToBeDeleted(config)} - /> - ), - }, + ...(canSave + ? [ + { + width: px(units.double), + name: '', + render: (config: Config) => ( + + ), + }, + { + width: px(units.double), + name: '', + render: (config: Config) => ( + setConfigToBeDeleted(config)} + /> + ), + }, + ] + : []), ]; return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index c408d5e960cf3..c1f5ec154792d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { EuiToolTip } from '@elastic/eui'; import { EuiButton, EuiFlexGroup, @@ -73,15 +74,35 @@ function CreateConfigurationButton() { const { basePath } = core.http; const { search } = useLocation(); const href = createAgentConfigurationHref(search, basePath); + const canSave = core.application.capabilities.apm.save; return ( - - {i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', { - defaultMessage: 'Create configuration', - })} - + + + {i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', { + defaultMessage: 'Create configuration', + })} + + diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 5a5d20cde9ade..ba08af32d65b6 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -4,25 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; import { - EuiTitle, + EuiButton, + EuiButtonEmpty, + EuiFieldText, EuiFlexGroup, EuiFlexItem, + EuiForm, + EuiFormRow, EuiPanel, EuiSpacer, EuiText, - EuiForm, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiButtonEmpty, + EuiTitle, + EuiToolTip, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useState } from 'react'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useFetcher } from '../../../../hooks/use_fetcher'; -import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { clearCache } from '../../../../services/rest/callApi'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; const APM_INDEX_LABELS = [ { @@ -85,17 +86,22 @@ async function saveApmIndices({ const INITIAL_STATE = [] as []; export function ApmIndices() { - const { toasts } = useApmPluginContext().core.notifications; + const { core } = useApmPluginContext(); + const { notifications, application } = core; + const canSave = application.capabilities.apm.save; const [apmIndices, setApmIndices] = useState>({}); const [isSaving, setIsSaving] = useState(false); - const { data = INITIAL_STATE, status, refetch } = useFetcher( - (_callApmApi) => - _callApmApi({ - endpoint: `GET /api/apm/settings/apm-index-settings`, - }), - [] + const { data = INITIAL_STATE, refetch } = useFetcher( + (_callApmApi) => { + if (canSave) { + return _callApmApi({ + endpoint: `GET /api/apm/settings/apm-index-settings`, + }); + } + }, + [canSave] ); useEffect(() => { @@ -119,7 +125,7 @@ export function ApmIndices() { setIsSaving(true); try { await saveApmIndices({ apmIndices }); - toasts.addSuccess({ + notifications.toasts.addSuccess({ title: i18n.translate( 'xpack.apm.settings.apmIndices.applyChanges.succeeded.title', { defaultMessage: 'Indices applied' } @@ -133,7 +139,7 @@ export function ApmIndices() { ), }); } catch (error) { - toasts.addDanger({ + notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.settings.apmIndices.applyChanges.failed.title', { defaultMessage: 'Indices could not be applied.' } @@ -205,6 +211,7 @@ export function ApmIndices() { fullWidth > - - {i18n.translate( - 'xpack.apm.settings.apmIndices.applyButton', - { defaultMessage: 'Apply changes' } - )} - + + {i18n.translate( + 'xpack.apm.settings.apmIndices.applyButton', + { defaultMessage: 'Apply changes' } + )} + + diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx index 56b3eaf425af7..3b4c127aab1e5 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx @@ -3,17 +3,40 @@ * 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 { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; export function CreateCustomLinkButton({ onClick }: { onClick: () => void }) { + const { core } = useApmPluginContext(); + const canSave = core.application.capabilities.apm.save; return ( - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.createCustomLink', - { defaultMessage: 'Create custom link' } - )} - + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.createCustomLink', + { defaultMessage: 'Create custom link' } + )} + + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx index d512ea19c7892..4bc1adee04bf4 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { isEmpty } from 'lodash'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { units, px } from '../../../../../style/variables'; import { ManagedTable } from '../../../../shared/ManagedTable'; @@ -26,6 +27,8 @@ interface Props { export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { const [searchTerm, setSearchTerm] = useState(''); + const { core } = useApmPluginContext(); + const canSave = core.application.capabilities.apm.save; const columns = [ { @@ -61,22 +64,26 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { width: px(units.triple), name: '', actions: [ - { - name: i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.table.editButtonLabel', - { defaultMessage: 'Edit' } - ), - description: i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.table.editButtonDescription', - { defaultMessage: 'Edit this custom link' } - ), - icon: 'pencil', - color: 'primary', - type: 'icon', - onClick: (customLink: CustomLink) => { - onCustomLinkSelected(customLink); - }, - }, + ...(canSave + ? [ + { + name: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.editButtonLabel', + { defaultMessage: 'Edit' } + ), + description: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.editButtonDescription', + { defaultMessage: 'Edit this custom link' } + ), + icon: 'pencil', + color: 'primary', + type: 'icon', + onClick: (customLink: CustomLink) => { + onCustomLinkSelected(customLink); + }, + }, + ] + : []), ], }, ]; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 1da7d415b5660..4477ee5a99be3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -7,22 +7,26 @@ import { fireEvent, render, - waitFor, RenderResult, + waitFor, } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import * as apmApi from '../../../../../services/rest/createCallApmApi'; +import { CustomLinkOverview } from '.'; import { License } from '../../../../../../../licensing/common/license'; -import * as hooks from '../../../../../hooks/use_fetcher'; +import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context'; +import { + mockApmPluginContextValue, + MockApmPluginContextWrapper, +} from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { LicenseContext } from '../../../../../context/license/license_context'; -import { CustomLinkOverview } from '.'; +import * as hooks from '../../../../../hooks/use_fetcher'; +import * as apmApi from '../../../../../services/rest/createCallApmApi'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; const data = [ { @@ -39,6 +43,16 @@ const data = [ }, ]; +function getMockAPMContext({ canSave }: { canSave: boolean }) { + return ({ + ...mockApmPluginContextValue, + core: { + ...mockApmPluginContextValue.core, + application: { capabilities: { apm: { save: canSave }, ml: {} } }, + }, + } as unknown) as ApmPluginContextValue; +} + describe('CustomLink', () => { beforeAll(() => { jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({}); @@ -70,9 +84,11 @@ describe('CustomLink', () => { }); it('shows when no link is available', () => { const component = render( - - - + + + + + ); expectTextsInDocument(component, ['No links found.']); }); @@ -91,6 +107,34 @@ describe('CustomLink', () => { jest.clearAllMocks(); }); + it('enables create button when user has writte privileges', () => { + const mockContext = getMockAPMContext({ canSave: true }); + + const { getByTestId } = render( + + + + + + ); + const createButton = getByTestId('createButton') as HTMLButtonElement; + expect(createButton.disabled).toBeFalsy(); + }); + + it('enables edit button on custom link table when user has writte privileges', () => { + const mockContext = getMockAPMContext({ canSave: true }); + + const { getAllByText } = render( + + + + + + ); + + expect(getAllByText('Edit').length).toEqual(2); + }); + it('shows a table with all custom link', () => { const component = render( @@ -108,9 +152,11 @@ describe('CustomLink', () => { }); it('checks if create custom link button is available and working', () => { + const mockContext = getMockAPMContext({ canSave: true }); + const { queryByText, getByText } = render( - + @@ -137,9 +183,10 @@ describe('CustomLink', () => { }); const openFlyout = () => { + const mockContext = getMockAPMContext({ canSave: true }); const component = render( - + @@ -173,9 +220,10 @@ describe('CustomLink', () => { }); it('deletes a custom link', async () => { + const mockContext = getMockAPMContext({ canSave: true }); const component = render( - + @@ -356,4 +404,34 @@ describe('CustomLink', () => { expectTextsNotInDocument(component, ['Start free 30-day trial']); }); }); + + describe('with read-only user', () => { + it('disables create custom link button', () => { + const mockContext = getMockAPMContext({ canSave: false }); + + const { getByTestId } = render( + + + + + + ); + const createButton = getByTestId('createButton') as HTMLButtonElement; + expect(createButton.disabled).toBeTruthy(); + }); + + it('removes edit button on custom link table', () => { + const mockContext = getMockAPMContext({ canSave: false }); + + const { queryAllByText } = render( + + + + + + ); + + expect(queryAllByText('Edit').length).toEqual(0); + }); + }); }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index acc2550930b8e..b185685f0720a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -4,26 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { - EuiPanel, - EuiTitle, - EuiText, - EuiSpacer, EuiButton, EuiFlexGroup, EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; -import { MLSingleMetricLink } from '../../../shared/Links/MachineLearningLinks/MLSingleMetricLink'; +import { MLExplorerLink } from '../../../shared/Links/MachineLearningLinks/MLExplorerLink'; import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink'; -import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; -import { LegacyJobsCallout } from './legacy_jobs_callout'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { AnomalyDetectionApiResponse } from './index'; +import { LegacyJobsCallout } from './legacy_jobs_callout'; type Jobs = AnomalyDetectionApiResponse['jobs']; @@ -44,14 +44,14 @@ const columns: Array> = [ { defaultMessage: 'Action' } ), render: (jobId: string) => ( - + {i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText', { defaultMessage: 'View job in ML', } )} - + ), }, ]; diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index e974f05fbe994..230b103c0062a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -10,9 +10,10 @@ import { EuiPageBody, EuiPageSideBar, EuiSideNav, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; @@ -30,6 +31,12 @@ export function Settings({ children, location }: SettingsProps) { const canAccessML = !!core.application.capabilities.ml?.canAccessML; const { search, pathname } = location; + const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + + const toggleOpenOnMobile = () => { + setisSideNavOpenOnMobile((prevState) => !prevState); + }; + function getSettingsHref(path: string) { return getAPMHref({ basePath, path: `/settings${path}`, search }); } @@ -41,16 +48,24 @@ export function Settings({ children, location }: SettingsProps) { > - - - {i18n.translate('xpack.apm.settings.returnLinkLabel', { - defaultMessage: 'Return to inventory', - })} - - + + + {i18n.translate('xpack.apm.settings.returnLinkLabel', { + defaultMessage: 'Return to inventory', + })} + + + toggleOpenOnMobile()} + isOpenOnMobile={isSideNavOpenOnMobile} items={[ { name: i18n.translate('xpack.apm.settings.pageTitle', { diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index e68f8a9809bf5..b216ab5498cf6 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -17,7 +17,7 @@ import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; -import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; +import { TransactionDetailLink } from '../../shared/Links/apm/transaction_detail_link'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TraceGroup = APIReturnType<'GET /api/apm/traces'>['items'][0]; @@ -44,7 +44,7 @@ const traceListColumns: Array> = [ _: string, { serviceName, transactionName, transactionType }: TraceGroup ) => ( - + { it('getFormattedBuckets', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 9a40d7834d18a..49a016f338888 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiButton, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; export const MaybeViewTraceLink = ({ @@ -18,6 +19,9 @@ export const MaybeViewTraceLink = ({ transaction: ITransaction; waterfall: IWaterfall; }) => { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const viewFullTraceButtonLabel = i18n.translate( 'xpack.apm.transactionDetails.viewFullTraceButtonLabel', { @@ -77,6 +81,7 @@ export const MaybeViewTraceLink = ({ traceId={rootTransaction.trace.id} transactionName={rootTransaction.transaction.name} transactionType={rootTransaction.transaction.type} + latencyAggregationType={latencyAggregationType} > {viewFullTraceButtonLabel} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts similarity index 90% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts index 72533cf2930d2..7666db35d43cf 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { getAgentMarks } from '../get_agent_marks'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; +import { getAgentMarks } from './get_agent_marks'; describe('getAgentMarks', () => { it('should sort the marks by time', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts similarity index 94% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts index abfecc3f70d24..0eb7a5b89aa3a 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IWaterfallError } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; -import { getErrorMarks } from '../get_error_marks'; +import { IWaterfallError } from '../Waterfall/waterfall_helpers/waterfall_helpers'; +import { getErrorMarks } from './get_error_marks'; describe('getErrorMarks', () => { describe('returns empty array', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx index b0ef28fbb7b0d..a67ec0a69ed87 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx @@ -6,20 +6,25 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useUrlParams } from '../../../../../../context/url_params_context/use_url_params'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../../../../../common/elasticsearch_fieldnames'; import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; -import { TransactionDetailLink } from '../../../../../shared/Links/apm/TransactionDetailLink'; +import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview_link'; +import { TransactionDetailLink } from '../../../../../shared/Links/apm/transaction_detail_link'; import { StickyProperties } from '../../../../../shared/StickyProperties'; -import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview'; interface Props { transaction?: Transaction; } export function FlyoutTopLevelProperties({ transaction }: Props) { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); + if (!transaction) { return null; } @@ -51,6 +56,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) { traceId={transaction.trace.id} transactionName={transaction.transaction.name} transactionType={transaction.transaction.type} + latencyAggregationType={latencyAggregationType} > {transaction.transaction.name} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx index ca5b4938ff42e..5a1f6e3d2a24d 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx @@ -6,17 +6,18 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; +import { useUrlParams } from '../../../../../../../context/url_params_context/use_url_params'; import { + SERVICE_NAME, SPAN_NAME, TRANSACTION_NAME, - SERVICE_NAME, } from '../../../../../../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; +import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview_link'; +import { TransactionDetailLink } from '../../../../../../shared/Links/apm/transaction_detail_link'; import { StickyProperties } from '../../../../../../shared/StickyProperties'; -import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview'; -import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink'; interface Props { span: Span; @@ -24,6 +25,9 @@ interface Props { } export function StickySpanProperties({ span, transaction }: Props) { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const spanName = span.span.name; const transactionStickyProperties = transaction ? [ @@ -56,6 +60,7 @@ export function StickySpanProperties({ span, transaction }: Props) { traceId={transaction.trace.id} transactionName={transaction.transaction.name} transactionType={transaction.transaction.type} + latencyAggregationType={latencyAggregationType} > {transaction.transaction.name} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 2806b8e989ee6..ae85b1370123f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -10,7 +10,6 @@ import { History, Location } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; -import { px } from '../../../../../../style/variables'; import { Timeline } from '../../../../../shared/charts/Timeline'; import { HeightRetainer } from '../../../../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; @@ -55,10 +54,7 @@ const toggleFlyout = ({ }); }; -const WaterfallItemsContainer = styled.div<{ - paddingTop: number; -}>` - padding-top: ${(props) => px(props.paddingTop)}; +const WaterfallItemsContainer = styled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; `; @@ -143,7 +139,7 @@ export function Waterfall({ margins={TIMELINE_MARGINS} />
- + {renderItems(waterfall.childrenByParentId)} diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 961320baa6a4e..fe3cb541617a3 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -10,20 +10,21 @@ import React, { ReactNode } from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { enableServiceOverview } from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink'; import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink'; import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink'; import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink'; import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link'; -import { useServiceOrTransactionsOverviewHref } from '../../shared/Links/apm/service_transactions_overview'; +import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link'; import { MainTabs } from '../../shared/main_tabs'; import { ErrorGroupOverview } from '../ErrorGroupOverview'; import { ServiceMap } from '../ServiceMap'; -import { ServiceMetrics } from '../service_metrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; +import { ServiceMetrics } from '../service_metrics'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface Tab { key: string; @@ -46,6 +47,9 @@ interface Props { export function ServiceDetailTabs({ serviceName, tab }: Props) { const { agentName } = useApmServiceContext(); const { uiSettings } = useApmPluginContext().core; + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const overviewTab = { key: 'overview', @@ -60,7 +64,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const transactionsTab = { key: 'transactions', - href: useServiceOrTransactionsOverviewHref(serviceName), + href: useTransactionsOverviewHref({ serviceName, latencyAggregationType }), text: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', { defaultMessage: 'Transactions', }), diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx index fa890260a3060..5fe371c33475a 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx @@ -17,7 +17,7 @@ import { px } from '../../../../style/variables'; interface IconPopoverProps { title: string; children: React.ReactChild; - onOpen: () => void; + onClick: () => void; onClose: () => void; detailsFetchStatus: FETCH_STATUS; isOpen: boolean; @@ -27,7 +27,7 @@ export function IconPopover({ icon, title, children, - onOpen, + onClick, onClose, detailsFetchStatus, isOpen, @@ -44,7 +44,7 @@ export function IconPopover({ anchorPosition="downCenter" ownFocus={false} button={ - + } diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx index 327198e46131f..f6a712c562bff 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx @@ -82,6 +82,7 @@ export function ServiceIcons({ serviceName }: Props) { (callApmApi) => { if (selectedIconPopover && serviceName && start && end) { return callApmApi({ + isCachable: true, endpoint: 'GET /api/apm/services/{serviceName}/metadata/details', params: { path: { serviceName }, @@ -143,8 +144,10 @@ export function ServiceIcons({ serviceName }: Props) { icon={item.icon} detailsFetchStatus={detailsFetchStatus} title={item.title} - onOpen={() => { - setSelectedIconPopover(item.key); + onClick={() => { + setSelectedIconPopover((prevSelectedIconPopover) => + item.key === prevSelectedIconPopover ? null : item.key + ); }} onClose={() => { setSelectedIconPopover(null); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/props.json b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/props.json deleted file mode 100644 index 2e213c44bccf0..0000000000000 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/props.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "items": [ - { - "serviceName": "opbeans-node", - "agentName": "nodejs", - "transactionsPerMinute": { - "value": 0, - "timeseries": [] - }, - "errorsPerMinute": { - "value": 46.06666666666667, - "timeseries": [] - }, - "environments": ["test"] - }, - { - "serviceName": "opbeans-python", - "agentName": "python", - "transactionsPerMinute": { - "value": 86.93333333333334, - "timeseries": [] - }, - "errorsPerMinute": { - "value": 12.6, - "timeseries": [] - }, - "avgResponseTime": { - "value": 91535.42944785276, - "timeseries": [] - }, - "environments": [] - } - ] -} diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/service_api_mock_data.ts b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/service_api_mock_data.ts new file mode 100644 index 0000000000000..04e1c9f8cbcab --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/service_api_mock_data.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; + +type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; + +export const items: ServiceListAPIResponse['items'] = [ + { + serviceName: 'opbeans-node', + transactionType: 'request', + agentName: 'nodejs', + transactionsPerMinute: { value: 0, timeseries: [] }, + transactionErrorRate: { value: 46.06666666666667, timeseries: [] }, + avgResponseTime: { value: null, timeseries: [] }, + environments: ['test'], + }, + { + serviceName: 'opbeans-python', + transactionType: 'page-load', + agentName: 'python', + transactionsPerMinute: { value: 86.93333333333334, timeseries: [] }, + transactionErrorRate: { value: 12.6, timeseries: [] }, + avgResponseTime: { value: 91535.42944785276, timeseries: [] }, + environments: [], + }, +]; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx index 157d3ecc738a1..1f8ff6fdcaf19 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx @@ -6,10 +6,16 @@ import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; +import { EuiIcon } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../../../common/transaction_types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { @@ -21,7 +27,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, px, truncate, unit } from '../../../../style/variables'; import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; -import { ServiceOrTransactionsOverviewLink } from '../../../shared/Links/apm/service_transactions_overview'; +import { ServiceOrTransactionsOverviewLink } from '../../../shared/Links/apm/service_transactions_overview_link'; import { AgentIcon } from '../../../shared/AgentIcon'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; @@ -55,126 +61,6 @@ const ToolTipWrapper = styled.span` } `; -export const SERVICE_COLUMNS: Array> = [ - { - field: 'healthStatus', - name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { - defaultMessage: 'Health', - }), - width: px(unit * 6), - sortable: true, - render: (_, { healthStatus }) => { - return ( - - ); - }, - }, - { - field: 'serviceName', - name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { - defaultMessage: 'Name', - }), - width: '40%', - sortable: true, - render: (_, { serviceName, agentName }) => ( - - - - {agentName && ( - - - - )} - - - {formatString(serviceName)} - - - - - - ), - }, - { - field: 'environments', - name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { - defaultMessage: 'Environment', - }), - width: px(unit * 10), - sortable: true, - render: (_, { environments }) => ( - - ), - }, - { - field: 'avgResponseTime', - name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', { - defaultMessage: 'Avg. response time', - }), - sortable: true, - dataType: 'number', - render: (_, { avgResponseTime }) => ( - - ), - align: 'left', - width: px(unit * 10), - }, - { - field: 'transactionsPerMinute', - name: i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel', - { - defaultMessage: 'Trans. per minute', - } - ), - sortable: true, - dataType: 'number', - render: (_, { transactionsPerMinute }) => ( - - ), - align: 'left', - width: px(unit * 10), - }, - { - field: 'transactionErrorRate', - name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { - defaultMessage: 'Error rate %', - }), - sortable: true, - dataType: 'number', - render: (_, { transactionErrorRate }) => { - const value = transactionErrorRate?.value; - - const valueLabel = asPercent(value, 1); - - return ( - - ); - }, - align: 'left', - width: px(unit * 10), - }, -]; - const SERVICE_HEALTH_STATUS_ORDER = [ ServiceHealthStatus.unknown, ServiceHealthStatus.healthy, @@ -182,59 +68,249 @@ const SERVICE_HEALTH_STATUS_ORDER = [ ServiceHealthStatus.critical, ]; +export function getServiceColumns({ + showTransactionTypeColumn, +}: { + showTransactionTypeColumn: boolean; +}): Array> { + return [ + { + field: 'healthStatus', + name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { + defaultMessage: 'Health', + }), + width: px(unit * 6), + sortable: true, + render: (_, { healthStatus }) => { + return ( + + ); + }, + }, + { + field: 'serviceName', + name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { + defaultMessage: 'Name', + }), + width: '40%', + sortable: true, + render: (_, { serviceName, agentName }) => ( + + + + {agentName && ( + + + + )} + + + {formatString(serviceName)} + + + + + + ), + }, + { + field: 'environments', + name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { + defaultMessage: 'Environment', + }), + width: px(unit * 10), + sortable: true, + render: (_, { environments }) => ( + + ), + }, + ...(showTransactionTypeColumn + ? [ + { + field: 'transactionType', + name: i18n.translate( + 'xpack.apm.servicesTable.transactionColumnLabel', + { + defaultMessage: 'Transaction type', + } + ), + width: px(unit * 10), + sortable: true, + }, + ] + : []), + { + field: 'avgResponseTime', + name: i18n.translate( + 'xpack.apm.servicesTable.avgResponseTimeColumnLabel', + { + defaultMessage: 'Avg. response time', + } + ), + sortable: true, + dataType: 'number', + render: (_, { avgResponseTime }) => ( + + ), + align: 'left', + width: px(unit * 10), + }, + { + field: 'transactionsPerMinute', + name: i18n.translate( + 'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel', + { + defaultMessage: 'Trans. per minute', + } + ), + sortable: true, + dataType: 'number', + render: (_, { transactionsPerMinute }) => ( + + ), + align: 'left', + width: px(unit * 10), + }, + { + field: 'transactionErrorRate', + name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { + defaultMessage: 'Error rate %', + }), + sortable: true, + dataType: 'number', + render: (_, { transactionErrorRate }) => { + const value = transactionErrorRate?.value; + + const valueLabel = asPercent(value, 1); + + return ( + + ); + }, + align: 'left', + width: px(unit * 10), + }, + ]; +} + export function ServiceList({ items, noItemsMessage }: Props) { const displayHealthStatus = items.some((item) => 'healthStatus' in item); + const showTransactionTypeColumn = items.some( + ({ transactionType }) => + transactionType !== TRANSACTION_REQUEST && + transactionType !== TRANSACTION_PAGE_LOAD + ); + + const serviceColumns = useMemo( + () => getServiceColumns({ showTransactionTypeColumn }), + [showTransactionTypeColumn] + ); + const columns = displayHealthStatus - ? SERVICE_COLUMNS - : SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus'); + ? serviceColumns + : serviceColumns.filter((column) => column.field !== 'healthStatus'); const initialSortField = displayHealthStatus ? 'healthStatus' : 'transactionsPerMinute'; return ( - { - // For healthStatus, sort items by healthStatus first, then by TPM - return sortField === 'healthStatus' - ? orderBy( - itemsToSort, - [ - (item) => { - return item.healthStatus - ? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus) - : -1; - }, - (item) => item.transactionsPerMinute?.value ?? 0, - ], - [sortDirection, sortDirection] - ) - : orderBy( - itemsToSort, - (item) => { - switch (sortField) { - // Use `?? -1` here so `undefined` will appear after/before `0`. - // In the table this will make the "N/A" items always at the - // bottom/top. - case 'avgResponseTime': - return item.avgResponseTime?.value ?? -1; - case 'transactionsPerMinute': - return item.transactionsPerMinute?.value ?? -1; - case 'transactionErrorRate': - return item.transactionErrorRate?.value ?? -1; - default: - return item[sortField as keyof typeof item]; + + + + + + )} + > + + + + + + {i18n.translate( + 'xpack.apm.servicesTable.metricsExplanationLabel', + { defaultMessage: 'What are these metrics?' } + )} + + + + + + { + // For healthStatus, sort items by healthStatus first, then by TPM + return sortField === 'healthStatus' + ? orderBy( + itemsToSort, + [ + (item) => { + return item.healthStatus + ? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus) + : -1; + }, + (item) => item.transactionsPerMinute?.value ?? 0, + ], + [sortDirection, sortDirection] + ) + : orderBy( + itemsToSort, + (item) => { + switch (sortField) { + // Use `?? -1` here so `undefined` will appear after/before `0`. + // In the table this will make the "N/A" items always at the + // bottom/top. + case 'avgResponseTime': + return item.avgResponseTime?.value ?? -1; + case 'transactionsPerMinute': + return item.transactionsPerMinute?.value ?? -1; + case 'transactionErrorRate': + return item.transactionErrorRate?.value ?? -1; + default: + return item[sortField as keyof typeof item]; + } + }, + sortDirection + ); + }} + /> + + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx index 1c6fa9fe0447e..45a4afeb53235 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx @@ -9,11 +9,8 @@ import { MemoryRouter } from 'react-router-dom'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { ServiceList, SERVICE_COLUMNS } from './'; -import props from './__fixtures__/props.json'; - -type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; +import { getServiceColumns, ServiceList } from './'; +import { items } from './__fixtures__/service_api_mock_data'; function Wrapper({ children }: { children?: ReactNode }) { return ( @@ -36,10 +33,7 @@ describe('ServiceList', () => { it('renders with data', () => { expect(() => - renderWithTheme( - , - { wrapper: Wrapper } - ) + renderWithTheme(, { wrapper: Wrapper }) ).not.toThrowError(); }); @@ -61,9 +55,9 @@ describe('ServiceList', () => { }, environments: ['test'], }; - const renderedColumns = SERVICE_COLUMNS.map((c) => - c.render!(service[c.field!], service) - ); + const renderedColumns = getServiceColumns({ + showTransactionTypeColumn: false, + }).map((c) => c.render!(service[c.field!], service)); expect(renderedColumns[0]).toMatchInlineSnapshot(` { describe('without ML data', () => { it('does not render the health column', () => { - const { queryByText } = renderWithTheme( - , - { - wrapper: Wrapper, - } - ); + const { queryByText } = renderWithTheme(, { + wrapper: Wrapper, + }); const healthHeading = queryByText('Health'); expect(healthHeading).toBeNull(); }); it('sorts by transactions per minute', async () => { - const { findByTitle } = renderWithTheme( - , - { - wrapper: Wrapper, - } - ); + const { findByTitle } = renderWithTheme(, { + wrapper: Wrapper, + }); expect( await findByTitle('Trans. per minute; Sorted in descending order') @@ -103,12 +91,10 @@ describe('ServiceList', () => { it('renders the health column', async () => { const { findByTitle } = renderWithTheme( ({ - ...item, - healthStatus: ServiceHealthStatus.warning, - }) - )} + items={items.map((item) => ({ + ...item, + healthStatus: ServiceHealthStatus.warning, + }))} />, { wrapper: Wrapper } ); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index 6bb1ea2919c16..e501dd3bb7a56 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -21,7 +21,7 @@ import { import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import * as useLocalUIFilters from '../../../hooks/useLocalUIFilters'; import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; -import { SessionStorageMock } from '../../../services/__test__/SessionStorageMock'; +import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import * as hook from './use_anomaly_detection_jobs_fetcher'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index c02f72245cdf5..46c2a4c322c92 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -23,6 +23,7 @@ import { ServiceOverview } from './'; import { waitFor } from '@testing-library/dom'; import * as callApmApiModule from '../../../services/rest/createCallApmApi'; import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -45,7 +46,11 @@ function Wrapper({ children }: { children?: ReactNode }) { {children} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index 4b262f1f51319..307997731e5ef 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -23,7 +23,6 @@ import { import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { useLatencyAggregationType } from '../../../../hooks/use_latency_Aggregation_type'; import { APIReturnType, callApmApi, @@ -31,8 +30,8 @@ import { import { px, unit } from '../../../../style/variables'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ImpactBar } from '../../../shared/ImpactBar'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; -import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_ink'; +import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; +import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_link'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; @@ -54,10 +53,16 @@ const DEFAULT_SORT = { field: 'impact' as const, }; -function getLatencyAggregationTypeLabel( - latencyAggregationType?: LatencyAggregationType -) { +function getLatencyAggregationTypeLabel(latencyAggregationType?: string) { switch (latencyAggregationType) { + case 'avg': { + i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', + { + defaultMessage: 'Latency (avg.)', + } + ); + } case 'p95': { return i18n.translate( 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95', @@ -74,24 +79,15 @@ function getLatencyAggregationTypeLabel( } ); } - default: { - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', - { - defaultMessage: 'Latency (avg.)', - } - ); - } } } export function ServiceOverviewTransactionsTable(props: Props) { const { serviceName } = props; const { transactionType } = useApmServiceContext(); - const latencyAggregationType = useLatencyAggregationType(); const { uiFilters, - urlParams: { start, end }, + urlParams: { start, end, latencyAggregationType }, } = useUrlParams(); const [tableOptions, setTableOptions] = useState<{ @@ -135,7 +131,7 @@ export function ServiceOverviewTransactionsTable(props: Props) { sortField: tableOptions.sort.field, sortDirection: tableOptions.sort.direction, transactionType, - latencyAggregationType, + latencyAggregationType: latencyAggregationType as LatencyAggregationType, }, }, }).then((response) => { @@ -187,6 +183,7 @@ export function ServiceOverviewTransactionsTable(props: Props) { serviceName={serviceName} transactionName={name} transactionType={type} + latencyAggregationType={latencyAggregationType} > {name} @@ -282,7 +279,10 @@ export function ServiceOverviewTransactionsTable(props: Props) { - + {i18n.translate( 'xpack.apm.serviceOverview.transactionsTableLinkText', { diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx index ade0a0563b0dc..1699b7e7474fe 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx @@ -8,6 +8,7 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asMillisecondDuration, @@ -18,7 +19,7 @@ import { ImpactBar } from '../../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups'>['items'][0]; @@ -40,6 +41,9 @@ interface Props { } export function TransactionList({ items, isLoading }: Props) { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const columns: Array> = useMemo( () => [ { @@ -58,6 +62,7 @@ export function TransactionList({ items, isLoading }: Props) { serviceName={serviceName} transactionName={transactionName} transactionType={transactionType} + latencyAggregationType={latencyAggregationType} > , }, ], - [] + [latencyAggregationType] ); const noItemsMessage = ( diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js b/x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js similarity index 95% rename from x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js rename to x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js index d4b3f223f726f..4e94ea85c120b 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { ImpactBar } from '..'; +import { ImpactBar } from '.'; describe('ImpactBar component', () => { it('should render with default values', () => { diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx index 5a9e8809ea734..a08ade8e559d0 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import { KeyValueTable } from '..'; +import { KeyValueTable } from '.'; import { render } from '@testing-library/react'; -import { renderWithTheme } from '../../../../utils/testHelpers'; +import { renderWithTheme } from '../../../utils/testHelpers'; function getKeys(output: ReturnType) { const keys = output.getAllByTestId('dot-key'); diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx index f71c8b71aa2ee..3a41c19c53f6d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx @@ -6,8 +6,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { DiscoverErrorLink } from '../DiscoverErrorLink'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { DiscoverErrorLink } from './DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { let wrapper: ShallowWrapper; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx index f71c8b71aa2ee..3a41c19c53f6d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx @@ -6,8 +6,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { DiscoverErrorLink } from '../DiscoverErrorLink'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { DiscoverErrorLink } from './DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { let wrapper: ShallowWrapper; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx index ca02abc395992..e77d4d7185273 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx @@ -6,13 +6,13 @@ import { Location } from 'history'; import React from 'react'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; -import { getRenderedHref } from '../../../../../utils/testHelpers'; -import { DiscoverErrorLink } from '../DiscoverErrorLink'; -import { DiscoverSpanLink } from '../DiscoverSpanLink'; -import { DiscoverTransactionLink } from '../DiscoverTransactionLink'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { getRenderedHref } from '../../../../utils/testHelpers'; +import { DiscoverErrorLink } from './DiscoverErrorLink'; +import { DiscoverSpanLink } from './DiscoverSpanLink'; +import { DiscoverTransactionLink } from './DiscoverTransactionLink'; describe('DiscoverLinks', () => { it('produces the correct URL for a transaction', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx similarity index 84% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx index 48d8bb2b41644..0ded3fb6619e3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; // @ts-expect-error import configureStore from '../../../../../store/config/configureStore'; -import { getDiscoverQuery } from '../DiscoverTransactionLink'; +import { getDiscoverQuery } from './DiscoverTransactionLink'; function getMockTransaction() { return { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mock_transaction.json b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__fixtures__/mock_transaction.json similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mock_transaction.json rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__fixtures__/mock_transaction.json diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorButton.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorButton.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorLink.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorLink.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverTransactionLink.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverTransactionLink.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/discover_transaction_button.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/discover_transaction_button.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/discover_transaction_button.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/discover_transaction_button.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/discover_transaction_button.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx similarity index 82% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/discover_transaction_button.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx index 4a68a5c0b4904..75fe18913618d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/discover_transaction_button.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx @@ -6,12 +6,12 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { DiscoverTransactionLink, getDiscoverQuery, -} from '../DiscoverTransactionLink'; -import mockTransaction from './mock_transaction.json'; +} from './DiscoverTransactionLink'; +import mockTransaction from './__fixtures__/mock_transaction.json'; describe('DiscoverTransactionLink component', () => { it('should render with data', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx new file mode 100644 index 0000000000000..3f02ed082f564 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Location } from 'history'; +import React from 'react'; +import { getRenderedHref } from '../../../../utils/testHelpers'; +import { MLExplorerLink } from './MLExplorerLink'; + +describe('MLExplorerLink', () => { + it('should produce the correct URL with jobId', async () => { + const href = await getRenderedHref( + () => ( + + ), + { + search: + '?rangeFrom=now/w&rangeTo=now-4h&refreshPaused=true&refreshInterval=0', + } as Location + ); + + expect(href).toMatchInlineSnapshot( + `"/app/ml/explorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now%2Fw,to:now-4h))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` + ); + }); + + it('correctly encodes time range values', async () => { + const href = await getRenderedHref( + () => ( + + ), + { + search: + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + } as Location + ); + + expect(href).toMatchInlineSnapshot( + `"/app/ml/explorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx new file mode 100644 index 0000000000000..ca9eb063bd090 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { TimePickerRefreshInterval } from '../../DatePicker/typings'; + +interface Props { + children?: ReactNode; + jobId: string; + external?: boolean; +} + +export function MLExplorerLink({ jobId, external, children }: Props) { + const href = useExplorerHref({ jobId }); + + return ( + + ); +} + +export function useExplorerHref({ jobId }: { jobId: string }) { + const { + core, + plugins: { ml }, + } = useApmPluginContext(); + const { urlParams } = useUrlParams(); + + const timePickerRefreshIntervalDefaults = core.uiSettings.get( + UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS + ); + + const { + // hardcoding a custom default of 1 hour since the default kibana timerange of 15 minutes is shorter than the ML interval + rangeFrom = 'now-1h', + rangeTo = 'now', + refreshInterval = timePickerRefreshIntervalDefaults.value, + refreshPaused = timePickerRefreshIntervalDefaults.pause, + } = urlParams; + + const href = useMlHref(ml, core.http.basePath.get(), { + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: [jobId], + timeRange: { from: rangeFrom, to: rangeTo }, + refreshInterval: { pause: refreshPaused, value: refreshInterval }, + }, + }); + + return href; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index de7130e878608..8031b6088d420 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -3,11 +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 { EuiLink } from '@elastic/eui'; import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -25,13 +24,6 @@ interface Props extends APMLinkExtendProps { } export function MetricOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); - - return ( - - ); + const href = useMetricOverviewHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx index ae5dc86608a90..670b7137219e1 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx @@ -3,15 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; - -function pathFor(serviceName?: string) { - return serviceName ? `/services/${serviceName}/service-map` : '/service-map'; -} +import { APMLinkExtendProps, useAPMHref } from './APMLink'; export function useServiceMapHref(serviceName?: string) { - return useAPMHref(pathFor(serviceName)); + const pathFor = serviceName + ? `/services/${serviceName}/service-map` + : '/service-map'; + return useAPMHref(pathFor); } interface ServiceMapLinkProps extends APMLinkExtendProps { @@ -19,6 +19,6 @@ interface ServiceMapLinkProps extends APMLinkExtendProps { } export function ServiceMapLink({ serviceName, ...rest }: ServiceMapLinkProps) { - const path = pathFor(serviceName); - return ; + const href = useServiceMapHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx index c107b436717c2..279c038d95a80 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx @@ -3,11 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; +import { useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -19,19 +16,3 @@ const persistedFilters: Array = [ export function useServiceNodeOverviewHref(serviceName: string) { return useAPMHref(`/services/${serviceName}/nodes`, persistedFilters); } - -interface Props extends APMLinkExtendProps { - serviceName: string; -} - -export function ServiceNodeOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx index caa1498e6df87..3cb0009a12c94 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx @@ -9,11 +9,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'transactionResult', @@ -25,15 +22,3 @@ const persistedFilters: Array = [ export function useTraceOverviewHref() { return useAPMHref('/traces', persistedFilters); } - -export function TraceOverviewLink(props: APMLinkExtendProps) { - const { urlParams } = useUrlParams(); - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx deleted file mode 100644 index ee798e0208c2b..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; - -interface Props extends APMLinkExtendProps { - serviceName: string; - traceId?: string; - transactionId?: string; - transactionName: string; - transactionType: string; -} - -export function TransactionDetailLink({ - serviceName, - traceId, - transactionId, - transactionName, - transactionType, - ...rest -}: Props) { - const { urlParams } = useUrlParams(); - - const persistedFilters = pickKeys( - urlParams, - 'transactionResult', - 'serviceVersion' - ); - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx index 318a1590be77c..c3b80cbeb701b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx @@ -9,22 +9,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { useAPMHref } from './APMLink'; const persistedFilters: Array = ['host', 'agentName']; export function useServiceInventoryHref() { return useAPMHref('/services', persistedFilters); } - -export function ServiceInventoryLink(props: APMLinkExtendProps) { - const { urlParams } = useUrlParams(); - - const query = pickKeys(urlParams as APMQueryParams, ...persistedFilters); - - return ; -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx index 1f74f1f9890cf..ba53243a6bc75 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx @@ -8,11 +8,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 { EuiLink } from '@elastic/eui'; import React from 'react'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; interface ServiceOverviewLinkProps extends APMLinkExtendProps { serviceName: string; @@ -30,13 +29,6 @@ export function ServiceOverviewLink({ serviceName, ...rest }: ServiceOverviewLinkProps) { - const { urlParams } = useUrlParams(); - - return ( - - ); + const href = useServiceOverviewHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx new file mode 100644 index 0000000000000..4c826ecf37682 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { Router } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; +import { + ServiceOrTransactionsOverviewLink, + useServiceOrTransactionsOverviewHref, +} from './service_transactions_overview_link'; + +const history = createMemoryHistory(); + +function wrapper({ queryParams }: { queryParams?: Record }) { + return ({ children }: { children: React.ReactElement }) => ( + + + + {children} + + + + ); +} + +describe('Service or transactions overview link', () => { + describe('useServiceOrTransactionsOverviewHref', () => { + it('returns service link', () => { + const { result } = renderHook( + () => useServiceOrTransactionsOverviewHref('foo'), + { wrapper: wrapper({}) } + ); + expect(result.current).toEqual('/basepath/app/apm/services/foo'); + }); + + it('returns service link with persisted query items', () => { + const { result } = renderHook( + () => useServiceOrTransactionsOverviewHref('foo'), + { wrapper: wrapper({ queryParams: { latencyAggregationType: 'avg' } }) } + ); + expect(result.current).toEqual( + '/basepath/app/apm/services/foo?latencyAggregationType=avg' + ); + }); + }); + describe('ServiceOrTransactionsOverviewLink', () => { + function getHref(container: HTMLElement) { + return ((container as HTMLDivElement).children[0] as HTMLAnchorElement) + .href; + } + it('returns service link', () => { + const Component = wrapper({}); + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo' + ); + }); + + it('returns service link with persisted query items', () => { + const Component = wrapper({ + queryParams: { latencyAggregationType: 'avg' }, + }); + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo?latencyAggregationType=avg' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx similarity index 59% rename from x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx index 24a78e5d64749..8b96ba8ab233a 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx @@ -3,11 +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 { EuiLink } from '@elastic/eui'; import React from 'react'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'transactionResult', @@ -19,7 +18,7 @@ const persistedFilters: Array = [ ]; export function useServiceOrTransactionsOverviewHref(serviceName: string) { - return useAPMHref(`/services/${serviceName}/transactions`, persistedFilters); + return useAPMHref(`/services/${serviceName}`, persistedFilters); } interface Props extends APMLinkExtendProps { @@ -30,13 +29,6 @@ export function ServiceOrTransactionsOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); - - return ( - - ); + const href = useServiceOrTransactionsOverviewHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx new file mode 100644 index 0000000000000..8108dcf41321f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { EuiLink } from '@elastic/eui'; +import { getAPMHref, APMLinkExtendProps } from './APMLink'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; +import { APMQueryParams } from '../url_helpers'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; + +interface Props extends APMLinkExtendProps { + serviceName: string; + traceId?: string; + transactionId?: string; + transactionName: string; + transactionType: string; + latencyAggregationType?: string; +} + +const persistedFilters: Array = [ + 'transactionResult', + 'serviceVersion', +]; + +export function TransactionDetailLink({ + serviceName, + traceId, + transactionId, + transactionName, + transactionType, + latencyAggregationType, + ...rest +}: Props) { + const { urlParams } = useUrlParams(); + const { core } = useApmPluginContext(); + const location = useLocation(); + const href = getAPMHref({ + basePath: core.http.basePath, + path: `/services/${serviceName}/transactions/view`, + query: { + traceId, + transactionId, + transactionName, + transactionType, + ...(latencyAggregationType ? { latencyAggregationType } : {}), + ...pickKeys(urlParams as APMQueryParams, ...persistedFilters), + }, + search: location.search, + }); + + return ; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx deleted file mode 100644 index d2978b3c02d53..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { APMQueryParams } from '../url_helpers'; - -interface Props extends APMLinkExtendProps { - serviceName: string; -} - -const persistedFilters: Array = [ - 'latencyAggregationType', -]; - -export function TransactionOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx new file mode 100644 index 0000000000000..5ca94884462db --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { Router } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; +import { + TransactionOverviewLink, + useTransactionsOverviewHref, +} from './transaction_overview_link'; + +const history = createMemoryHistory(); + +function Wrapper({ children }: { children: React.ReactElement }) { + return ( + + + {children} + + + ); +} + +describe('Transactions overview link', () => { + describe('useTransactionsOverviewHref', () => { + it('returns transaction link', () => { + const { result } = renderHook( + () => useTransactionsOverviewHref({ serviceName: 'foo' }), + { wrapper: Wrapper } + ); + expect(result.current).toEqual( + '/basepath/app/apm/services/foo/transactions' + ); + }); + + it('returns transaction link with persisted query items', () => { + const { result } = renderHook( + () => + useTransactionsOverviewHref({ + serviceName: 'foo', + latencyAggregationType: 'avg', + }), + { wrapper: Wrapper } + ); + expect(result.current).toEqual( + '/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg' + ); + }); + }); + describe('TransactionOverviewLink', () => { + function getHref(container: HTMLElement) { + return ((container as HTMLDivElement).children[0] as HTMLAnchorElement) + .href; + } + it('returns transaction link', () => { + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo/transactions' + ); + }); + + it('returns transaction link with persisted query items', () => { + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx new file mode 100644 index 0000000000000..dd53c5ab15260 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.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 { EuiLink } from '@elastic/eui'; +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { APMLinkExtendProps, getAPMHref } from './APMLink'; + +interface Props extends APMLinkExtendProps { + serviceName: string; + latencyAggregationType?: string; +} + +export function useTransactionsOverviewHref({ + serviceName, + latencyAggregationType, +}: { + serviceName: string; + latencyAggregationType?: string; +}) { + const { core } = useApmPluginContext(); + const location = useLocation(); + const { search } = location; + + return getAPMHref({ + basePath: core.http.basePath, + path: `/services/${serviceName}/transactions`, + query: { ...(latencyAggregationType ? { latencyAggregationType } : {}) }, + search, + }); +} + +export function TransactionOverviewLink({ + serviceName, + latencyAggregationType, + ...rest +}: Props) { + const href = useTransactionsOverviewHref({ + serviceName, + latencyAggregationType, + }); + return ; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index aa3881b81cc3f..8576d9ee86353 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -6,7 +6,6 @@ import { History } from 'history'; import { parse, stringify } from 'query-string'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; import { LocalUIFilterName } from '../../../../common/ui_filter'; @@ -85,7 +84,7 @@ export type APMQueryParams = { refreshInterval?: string | number; searchTerm?: string; percentile?: 50 | 75 | 90 | 95 | 99; - latencyAggregationType?: LatencyAggregationType; + latencyAggregationType?: string; } & { [key in LocalUIFilterName]?: string }; // forces every value of T[K] to be type: string diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js b/x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js similarity index 96% rename from x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js rename to x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js index 38f260b04e252..88e1c57e62354 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { UnoptimizedManagedTable } from '..'; +import { UnoptimizedManagedTable } from '.'; describe('ManagedTable component', () => { let people; diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap b/x-pack/plugins/apm/public/components/shared/ManagedTable/__snapshots__/ManagedTable.test.js.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ManagedTable/__snapshots__/ManagedTable.test.js.snap diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx index 8f44d98cecdf7..8a50bc2cde520 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx @@ -7,13 +7,13 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { ErrorMetadata } from '..'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { ErrorMetadata } from '.'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../../utils/testHelpers'; +} from '../../../../utils/testHelpers'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx index 8a4cd588c8260..9bd3278033f92 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx @@ -7,10 +7,10 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MetadataTable } from '..'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { expectTextsInDocument } from '../../../../utils/testHelpers'; -import { SectionsWithRows } from '../helper'; +import { MetadataTable } from '.'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { expectTextsInDocument } from '../../../utils/testHelpers'; +import { SectionsWithRows } from './helper'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx similarity index 83% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx index 7a150f81580d8..3dd19778430b7 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import { Section } from '../Section'; -import { expectTextsInDocument } from '../../../../utils/testHelpers'; +import { Section } from './Section'; +import { expectTextsInDocument } from '../../../utils/testHelpers'; describe('Section', () => { it('shows "empty state message" if no data is available', () => { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx index c97e506187347..c9ed2c4c2b32f 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx @@ -7,13 +7,13 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { SpanMetadata } from '..'; -import { Span } from '../../../../../../typings/es_schemas/ui/span'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { SpanMetadata } from '.'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../../utils/testHelpers'; +} from '../../../../utils/testHelpers'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx index 4080a300ba17f..6a5a122f23954 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx @@ -7,13 +7,13 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { TransactionMetadata } from '..'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { TransactionMetadata } from '.'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../../utils/testHelpers'; +} from '../../../../utils/testHelpers'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts similarity index 92% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts index ac776e0b8980c..8f3e675c7aeae 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSectionsWithRows, filterSectionsByTerm } from '../helper'; -import { LABELS, HTTP, SERVICE } from '../sections'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { getSectionsWithRows, filterSectionsByTerm } from './helper'; +import { LABELS, HTTP, SERVICE } from './sections'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; describe('MetadataTable Helper', () => { const sections = [ diff --git a/x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx index 26087e1fd85cc..fd531f79c9ac6 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx @@ -5,11 +5,11 @@ */ import React from 'react'; -import { ErrorCountSummaryItemBadge } from '../ErrorCountSummaryItemBadge'; +import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; import { expectTextsInDocument, renderWithTheme, -} from '../../../../utils/testHelpers'; +} from '../../../utils/testHelpers'; describe('ErrorCountSummaryItemBadge', () => { it('shows singular error message', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx index d0e1f08aabbbc..9465d94e16dc8 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; -import { HttpInfoSummaryItem } from '../'; -import * as exampleTransactions from '../../__fixtures__/transactions'; +import { HttpInfoSummaryItem } from '.'; +import * as exampleTransactions from '../__fixtures__/transactions'; describe('HttpInfoSummaryItem', () => { describe('render', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx index ecbf41486a3fd..0df23883d3127 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { mount } from 'enzyme'; -import { HttpStatusBadge } from '../index'; +import { HttpStatusBadge } from './index'; import { successColor, neutralColor, warningColor, errorColor, -} from '../../../../../utils/httpStatusCodeToColor'; +} from '../../../../utils/httpStatusCodeToColor'; describe('HttpStatusBadge', () => { describe('render', () => { diff --git a/x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx index b4678b287dc16..dd36827ea94f2 100644 --- a/x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import moment from 'moment-timezone'; -import { TimestampTooltip } from '../index'; -import { mockNow } from '../../../../utils/testHelpers'; +import { TimestampTooltip } from './index'; +import { mockNow } from '../../../utils/testHelpers'; describe('TimestampTooltip', () => { const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx index 8cb863c8fc385..6ff395db594f1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx @@ -7,18 +7,18 @@ import { act, fireEvent, render } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { License } from '../../../../../../licensing/common/license'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { LicenseContext } from '../../../../context/license/license_context'; -import * as hooks from '../../../../hooks/use_fetcher'; -import * as apmApi from '../../../../services/rest/createCallApmApi'; +import { License } from '../../../../../licensing/common/license'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { LicenseContext } from '../../../context/license/license_context'; +import * as hooks from '../../../hooks/use_fetcher'; +import * as apmApi from '../../../services/rest/createCallApmApi'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; -import { TransactionActionMenu } from '../TransactionActionMenu'; -import * as Transactions from './mockData'; +} from '../../../utils/testHelpers'; +import { TransactionActionMenu } from './TransactionActionMenu'; +import * as Transactions from './__fixtures__/mockData'; function Wrapper({ children }: { children?: React.ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 3f74b80bab064..312513db80886 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -63,7 +63,13 @@ export function TransactionActionMenu({ transaction }: Props) { isOpen={isActionPopoverOpen} anchorPosition="downRight" button={ - setIsActionPopoverOpen(true)} /> + + setIsActionPopoverOpen( + (prevIsActionPopoverOpen) => !prevIsActionPopoverOpen + ) + } + /> } >
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__fixtures__/mockData.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__fixtures__/mockData.ts diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts similarity index 98% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts index 048ae9474c403..f6067a34e2b90 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts @@ -5,8 +5,8 @@ */ import { Location } from 'history'; import { IBasePath } from 'kibana/public'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { getSections } from '../sections'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { getSections } from './sections'; describe('Transaction action menu', () => { const basePath = ({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index afc8951f121ea..2c71e75994a4a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -109,7 +109,7 @@ export function TimeseriesChart({ }} onPointerUpdate={setPointerEvent} externalPointerEvents={{ - tooltip: { visible: true, placement: Placement.Bottom }, + tooltip: { visible: true, placement: Placement.Right }, }} showLegend showLegendExtra diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index d125af70268cb..33dcbf02ccda7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -10,6 +10,7 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { MLSingleMetricLink } from '../../Links/MachineLearningLinks/MLSingleMetricLink'; @@ -33,12 +34,13 @@ const ShiftedEuiText = styled(EuiText)` export function MLHeader({ hasValidMlLicense, mlJobId }: Props) { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); + const { transactionType } = useApmServiceContext(); if (!hasValidMlLicense || !mlJobId) { return null; } - const { kuery, transactionType } = urlParams; + const { kuery } = urlParams; const hasKuery = !isEmpty(kuery); const icon = hasKuery ? ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 3f10b572b42a3..90877a895b05b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -36,7 +36,7 @@ export function TransactionErrorRateChart({ const { start, end, transactionName } = urlParams; const { data, status } = useFetcher(() => { - if (serviceName && start && end) { + if (transactionType && serviceName && start && end) { return callApmApi({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/error_rate', diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index ee0ea7f601f62..6d9f982f92751 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -5,6 +5,7 @@ */ import { Location } from 'history'; +import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { pickKeys } from '../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; @@ -48,7 +49,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { environment, searchTerm, percentile, - latencyAggregationType, + latencyAggregationType = LatencyAggregationType.avg, } = query; const localUIFilters = pickKeys(query, ...localUIFilterNames); diff --git a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.test.ts b/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.test.ts deleted file mode 100644 index 901877ca67460..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LatencyAggregationType } from '../../common/latency_aggregation_types'; -import { UIFilters } from '../../typings/ui_filters'; -import { IUrlParams } from '../context/url_params_context/types'; -import * as urlParams from '../context/url_params_context/use_url_params'; -import { useLatencyAggregationType } from './use_latency_Aggregation_type'; - -describe('useLatencyAggregationType', () => { - afterAll(() => { - jest.clearAllMocks(); - }); - it('returns avg when no value was given', () => { - jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({ - urlParams: { latencyAggregationType: undefined } as IUrlParams, - refreshTimeRange: jest.fn(), - uiFilters: {} as UIFilters, - }); - const latencyAggregationType = useLatencyAggregationType(); - expect(latencyAggregationType).toEqual(LatencyAggregationType.avg); - }); - - it('returns avg when no value does not match any of the availabe options', () => { - jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({ - urlParams: { latencyAggregationType: 'invalid_type' } as IUrlParams, - refreshTimeRange: jest.fn(), - uiFilters: {} as UIFilters, - }); - const latencyAggregationType = useLatencyAggregationType(); - expect(latencyAggregationType).toEqual(LatencyAggregationType.avg); - }); - - it('returns the value in the url', () => { - jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({ - urlParams: { latencyAggregationType: 'p95' } as IUrlParams, - refreshTimeRange: jest.fn(), - uiFilters: {} as UIFilters, - }); - const latencyAggregationType = useLatencyAggregationType(); - expect(latencyAggregationType).toEqual(LatencyAggregationType.p95); - }); -}); diff --git a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.ts b/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.ts deleted file mode 100644 index 72d07c9e4c22c..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LatencyAggregationType } from '../../common/latency_aggregation_types'; -import { useUrlParams } from '../context/url_params_context/use_url_params'; - -export function useLatencyAggregationType(): LatencyAggregationType { - const { - urlParams: { latencyAggregationType }, - } = useUrlParams(); - - if (!latencyAggregationType) { - return LatencyAggregationType.avg; - } - - if (latencyAggregationType in LatencyAggregationType) { - return latencyAggregationType as LatencyAggregationType; - } - - return LatencyAggregationType.avg; -} diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts index 7b1e7b06ac283..de3e68620b6e4 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts @@ -11,15 +11,14 @@ import { useUrlParams } from '../context/url_params_context/use_url_params'; import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; import { getLatencyChartSelector } from '../selectors/latency_chart_selectors'; import { useTheme } from './use_theme'; -import { useLatencyAggregationType } from './use_latency_Aggregation_type'; +import { LatencyAggregationType } from '../../common/latency_aggregation_types'; export function useTransactionLatencyChartsFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { transactionType } = useApmServiceContext(); - const latencyAggregationType = useLatencyAggregationType(); const theme = useTheme(); const { - urlParams: { start, end, transactionName }, + urlParams: { start, end, transactionName, latencyAggregationType }, uiFilters, } = useUrlParams(); @@ -43,7 +42,7 @@ export function useTransactionLatencyChartsFetcher() { transactionType, transactionName, uiFilters: JSON.stringify(uiFilters), - latencyAggregationType, + latencyAggregationType: latencyAggregationType as LatencyAggregationType, }, }, }); diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts index c03bb8efc79b3..e030d514a5283 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts @@ -10,18 +10,20 @@ import { useFetcher } from './use_fetcher'; import { useUrlParams } from '../context/url_params_context/use_url_params'; import { getThrouputChartSelector } from '../selectors/throuput_chart_selectors'; import { useTheme } from './use_theme'; +import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; export function useTransactionThroughputChartsFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); + const { transactionType } = useApmServiceContext(); const theme = useTheme(); const { - urlParams: { transactionType, start, end, transactionName }, + urlParams: { start, end, transactionName }, uiFilters, } = useUrlParams(); const { data, error, status } = useFetcher( (callApmApi) => { - if (serviceName && start && end) { + if (transactionType && serviceName && start && end) { return callApmApi({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/throughput', diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts index dee92bbffd27a..a5c25cfa3e07c 100644 --- a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { rgba } from 'polished'; import { EuiTheme } from '../../../observability/public'; -import { LatencyAggregationType } from '../../common/latency_aggregation_types'; import { asDuration } from '../../common/utils/formatters'; import { Coordinate, @@ -33,7 +32,7 @@ export function getLatencyChartSelector({ }: { latencyChart?: LatencyChartsResponse; theme: EuiTheme; - latencyAggregationType?: LatencyAggregationType; + latencyAggregationType?: string; }): LatencyChart { if (!latencyChart?.latencyTimeseries || !latencyAggregationType) { return { @@ -63,7 +62,7 @@ function getLatencyTimeseries({ }: { latencyChart: LatencyChartsResponse; theme: EuiTheme; - latencyAggregationType: LatencyAggregationType; + latencyAggregationType: string; }) { const { overallAvgDuration } = latencyChart; const { latencyTimeseries } = latencyChart; diff --git a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts index ac85142f3050b..03877b9e5bff2 100644 --- a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts +++ b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts @@ -37,6 +37,14 @@ describe('getThrouputChartSelector', () => { expect(throughputTimeseries).toEqual({ throughputTimeseries: [] }); }); + it('returns default values when timeseries is empty', () => { + const throughputTimeseries = getThrouputChartSelector({ + theme, + throuputChart: { throughputTimeseries: [] }, + }); + expect(throughputTimeseries).toEqual({ throughputTimeseries: [] }); + }); + it('return throughput time series', () => { const throughputTimeseries = getThrouputChartSelector({ theme, diff --git a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts index 701558b154677..a392f247aec42 100644 --- a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts @@ -8,7 +8,6 @@ import { difference, zipObject } from 'lodash'; import { EuiTheme } from '../../../observability/public'; import { asTransactionRate } from '../../common/utils/formatters'; import { TimeSeries } from '../../typings/timeseries'; -import { getEmptySeries } from '../components/shared/charts/helper/get_empty_series'; import { APIReturnType } from '../services/rest/createCallApmApi'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; @@ -34,7 +33,7 @@ export function getThrouputChartSelector({ }; } -export function getThroughputTimeseries({ +function getThroughputTimeseries({ throuputChart, theme, }: { @@ -45,15 +44,6 @@ export function getThroughputTimeseries({ const bucketKeys = throughputTimeseries.map(({ key }) => key); const getColor = getColorByKey(bucketKeys, theme); - if (!throughputTimeseries.length) { - const start = throughputTimeseries[0].dataPoints[0].x; - const end = - throughputTimeseries[0].dataPoints[ - throughputTimeseries[0].dataPoints.length - 1 - ].x; - return getEmptySeries(start, end); - } - return throughputTimeseries.map((bucket) => { return { title: bucket.key, diff --git a/x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts b/x-pack/plugins/apm/public/services/__mocks__/SessionStorageMock.ts similarity index 100% rename from x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts rename to x-pack/plugins/apm/public/services/__mocks__/SessionStorageMock.ts diff --git a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/plugins/apm/public/services/callApi.test.ts similarity index 97% rename from x-pack/plugins/apm/public/services/__test__/callApi.test.ts rename to x-pack/plugins/apm/public/services/callApi.test.ts index f82201bbd4de8..1e606ac4b9aa9 100644 --- a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/plugins/apm/public/services/callApi.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockNow } from '../../utils/testHelpers'; -import { clearCache, callApi } from '../rest/callApi'; -import { SessionStorageMock } from './SessionStorageMock'; +import { mockNow } from '../utils/testHelpers'; +import { clearCache, callApi } from './rest/callApi'; +import { SessionStorageMock } from './__mocks__/SessionStorageMock'; import { HttpSetup } from 'kibana/public'; type HttpMock = HttpSetup & { diff --git a/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/plugins/apm/public/services/callApmApi.test.ts similarity index 93% rename from x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts rename to x-pack/plugins/apm/public/services/callApmApi.test.ts index 2307ec9f06bb5..5906053cbd810 100644 --- a/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts +++ b/x-pack/plugins/apm/public/services/callApmApi.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as callApiExports from '../rest/callApi'; -import { createCallApmApi, callApmApi } from '../rest/createCallApmApi'; +import * as callApiExports from './rest/callApi'; +import { createCallApmApi, callApmApi } from './rest/createCallApmApi'; import { HttpSetup } from 'kibana/public'; const callApi = jest diff --git a/x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts b/x-pack/plugins/apm/public/utils/flattenObject.test.ts similarity index 96% rename from x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts rename to x-pack/plugins/apm/public/utils/flattenObject.test.ts index a71ecf73bad3f..68f77573949ea 100644 --- a/x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts +++ b/x-pack/plugins/apm/public/utils/flattenObject.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flattenObject } from '../flattenObject'; +import { flattenObject } from './flattenObject'; describe('FlattenObject', () => { it('flattens multi level item', () => { diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts index 161d5d03fcb40..4d1f53c9d4f94 100644 --- a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -4,14 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ThresholdMetActionGroupId } from '../../../common/alert_types'; import { ESSearchRequest, ESSearchResponse, } from '../../../../../typings/elasticsearch'; -import { AlertServices } from '../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../alerts/server'; export function alertingEsClient( - services: AlertServices, + services: AlertServices< + AlertInstanceState, + AlertInstanceContext, + ThresholdMetActionGroupId + >, params: TParams ): Promise> { return services.callCluster('search', { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 36fdf45d805f1..764e706834a70 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -4,13 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; -import { AlertingPlugin } from '../../../../alerts/server'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertingPlugin, + AlertInstanceContext, + AlertInstanceState, + AlertTypeState, +} from '../../../../alerts/server'; +import { + AlertType, + ALERT_TYPES_CONFIG, + ThresholdMetActionGroupId, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -41,7 +50,13 @@ export function registerErrorCountAlertType({ alerts, config$, }: RegisterAlertParams) { - alerts.registerType({ + alerts.registerType< + TypeOf, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + ThresholdMetActionGroupId + >({ id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index 62fc16fb25053..6d91e64be034d 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -159,7 +159,7 @@ export async function createApmTelemetry({ logger.debug( `Stored telemetry is out of date. Task will run immediately. Stored: ${currentData.kibanaVersion}, expected: ${kibanaVersion}` ); - taskManagerStart.runNow(APM_TELEMETRY_TASK_NAME); + await taskManagerStart.runNow(APM_TELEMETRY_TASK_NAME); } } catch (err) { if (!SavedObjectsErrorHelpers.isNotFoundError(err)) { diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap similarity index 100% rename from x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap rename to x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts rename to x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts index ff7d05efc1802..e05e7d3df2828 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getBuckets } from '../get_buckets'; -import { APMConfig } from '../../../..'; -import { ProcessorEvent } from '../../../../../common/processor_event'; +import { getBuckets } from './get_buckets'; +import { APMConfig } from '../../..'; +import { ProcessorEvent } from '../../../../common/processor_event'; describe('get buckets', () => { let clientSpy: jest.Mock; diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts similarity index 80% rename from x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts rename to x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts index a319bba1eabe1..711790d0c4aae 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getEnvironmentUiFilterES } from '../get_environment_ui_filter_es'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; -import { SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; +import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; +import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; describe('getEnvironmentUiFilterES', () => { it('should return empty array, when environment is undefined', () => { diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 536be56e152a3..876fc6b822213 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -18,7 +18,10 @@ export function getOutcomeAggregation({ searchAggregatedTransactions: boolean; }) { return { - terms: { field: EVENT_OUTCOME }, + terms: { + field: EVENT_OUTCOME, + include: [EventOutcome.failure, EventOutcome.success], + }, aggs: { // simply using the doc count to get the number of requests is not possible for transaction metrics (histograms) // to work around this we get the number of transactions by counting the number of latency values diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 76a718bbb2a02..68bdc4b3d0ae8 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -132,7 +132,9 @@ export async function getWebCoreVitals({ return { coreVitalPages: coreVitalPages?.doc_count ?? 0, - cls: cls?.values[pkey]?.toFixed(3) || null, + /* Because cls is required in the type UXMetrics, and defined as number | null, + * we need to default to null in the case where cls is undefined in order to satisfy the UXMetrics type */ + cls: cls?.values[pkey] ?? null, fid: fid?.values[pkey], lcp: lcp?.values[pkey], tbt: tbt?.values[pkey] ?? 0, diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 14047f4bacea9..f40ae7803e364 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { rangeFilter } from '../../../common/utils/range_filter'; import { ProcessorEvent } from '../../../common/processor_event'; import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; import { @@ -10,14 +11,19 @@ import { ExternalConnectionNode, ServiceConnectionNode, } from '../../../common/service_map'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function fetchServicePathsFromTraceIds( - setup: Setup, + setup: Setup & SetupTimeRange, traceIds: string[] ) { const { apmEventClient } = setup; + // make sure there's a range so ES can skip shards + const dayInMs = 24 * 60 * 60 * 1000; + const start = setup.start - dayInMs; + const end = setup.end + dayInMs; + const serviceMapParams = { apm: { events: [ProcessorEvent.span, ProcessorEvent.transaction], @@ -32,6 +38,7 @@ export async function fetchServicePathsFromTraceIds( [TRACE_ID]: traceIds, }, }, + { range: rangeFilter(start, end) }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts index 14cfece22d053..b650602062c0b 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts @@ -10,7 +10,7 @@ import { SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; import { Connection, ConnectionNode } from '../../../common/service_map'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { fetchServicePathsFromTraceIds } from './fetch_service_paths_from_trace_ids'; export function getConnections({ @@ -79,7 +79,7 @@ export async function getServiceMapFromTraceIds({ serviceName, environment, }: { - setup: Setup; + setup: Setup & SetupTimeRange; traceIds: string[]; serviceName?: string; environment?: string; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index d5e29532e3d7b..ca58a1b0e7126 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getServiceNodesProjection } from '../../projections/service_nodes'; -import { mergeProjection } from '../../projections/util/merge_projection'; -import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { - METRIC_PROCESS_CPU_PERCENT, - METRIC_JAVA_THREAD_COUNT, METRIC_JAVA_HEAP_MEMORY_USED, METRIC_JAVA_NON_HEAP_MEMORY_USED, + METRIC_JAVA_THREAD_COUNT, + METRIC_PROCESS_CPU_PERCENT, } from '../../../common/elasticsearch_fieldnames'; +import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; const getServiceNodes = async ({ setup, @@ -68,15 +68,21 @@ const getServiceNodes = async ({ return []; } - return response.aggregations.nodes.buckets.map((bucket) => { - return { + return response.aggregations.nodes.buckets + .map((bucket) => ({ name: bucket.key as string, cpu: bucket.cpu.value, heapMemory: bucket.heapMemory.value, nonHeapMemory: bucket.nonHeapMemory.value, threadCount: bucket.threadCount.value, - }; - }); + })) + .filter( + (item) => + item.cpu !== null || + item.heapMemory !== null || + item.nonHeapMemory !== null || + item.threadCount != null + ); }; export { getServiceNodes }; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index a6818f96c728e..21402e4c8dac0 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -100,196 +100,27 @@ Array [ "aggs": Object { "services": Object { "aggs": Object { - "average": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "timeseries": Object { + "transactionType": Object { "aggs": Object { - "average": Object { - "avg": Object { - "field": "transaction.duration.us", + "agentName": Object { + "top_hits": Object { + "docvalue_fields": Array [ + "agent.name", + ], + "size": 1, }, }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "43200s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - "metric", - "error", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "agent_name": Object { - "top_hits": Object { - "_source": Array [ - "agent.name", - ], - "size": 1, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - "timeseries": Object { - "aggs": Object { - "count": Object { - "value_count": Object { + "avg_duration": Object { + "avg": Object { "field": "transaction.duration.us", }, }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "43200s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "outcomes": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "", }, }, - }, - "terms": Object { - "field": "event.outcome", - }, - }, - "timeseries": Object { - "aggs": Object { "outcomes": Object { "aggs": Object { "count": Object { @@ -300,73 +131,62 @@ Array [ }, "terms": Object { "field": "event.outcome", + "include": Array [ + "failure", + "success", + ], }, }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, + "real_document_count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, }, - "field": "@timestamp", - "fixed_interval": "43200s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, + "timeseries": Object { + "aggs": Object { + "avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "outcomes": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "event.outcome", + "include": Array [ + "failure", + "success", + ], + }, + }, + "real_document_count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, }, }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - Object { - "terms": Object { - "event.outcome": Array [ - "failure", - "success", - ], - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - "metric", - "error", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "environments": Object { "terms": Object { - "field": "service.environment", - "size": 100, + "field": "transaction.type", + "order": Object { + "real_document_count": "desc", + }, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts index 15ed46a23cae8..937155bc31602 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts @@ -87,11 +87,6 @@ export async function getTimeseriesDataForTransactionGroups({ size, }, aggs: { - transaction_types: { - terms: { - field: TRANSACTION_TYPE, - }, - }, timeseries: { date_histogram: { field: '@timestamp', diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts index 41df70c9d5429..2df4b4e4c31f5 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts @@ -80,6 +80,7 @@ export async function getServiceTransactionGroups({ start, end, latencyAggregationType, + transactionType, }), totalTransactionGroups, isAggregationAccurate, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts index 6d9fc2fd3d579..a8794e3c09a40 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts @@ -4,17 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; - -import { - TRANSACTION_PAGE_LOAD, - TRANSACTION_REQUEST, -} from '../../../../common/transaction_types'; +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { getLatencyValue } from '../../helpers/latency_aggregation_type'; - import { TransactionGroupTimeseriesData } from './get_timeseries_data_for_transaction_groups'; - import { TransactionGroupWithoutTimeseriesData } from './get_transaction_groups_for_page'; export function mergeTransactionGroupData({ @@ -23,12 +16,14 @@ export function mergeTransactionGroupData({ transactionGroups, timeseriesData, latencyAggregationType, + transactionType, }: { start: number; end: number; transactionGroups: TransactionGroupWithoutTimeseriesData[]; timeseriesData: TransactionGroupTimeseriesData; latencyAggregationType: LatencyAggregationType; + transactionType: string; }) { const deltaAsMinutes = (end - start) / 1000 / 60; @@ -37,16 +32,6 @@ export function mergeTransactionGroupData({ ({ key }) => key === transactionGroup.name ); - const transactionTypes = - groupBucket?.transaction_types.buckets.map( - (bucket) => bucket.key as string - ) ?? []; - - const transactionType = - transactionTypes.find( - (type) => type === TRANSACTION_PAGE_LOAD || type === TRANSACTION_REQUEST - ) ?? transactionTypes[0]; - const timeseriesBuckets = groupBucket?.timeseries.buckets ?? []; return timeseriesBuckets.reduce( diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts new file mode 100644 index 0000000000000..206827a744113 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSeverity } from '../../../../common/anomaly_detection'; +import { getServiceHealthStatus } from '../../../../common/service_health_status'; +import { + getMLJobIds, + getServiceAnomalies, +} from '../../service_map/get_service_anomalies'; +import { + ServicesItemsProjection, + ServicesItemsSetup, +} from './get_services_items'; + +interface AggregationParams { + setup: ServicesItemsSetup; + projection: ServicesItemsProjection; + searchAggregatedTransactions: boolean; +} + +export const getHealthStatuses = async ( + { setup }: AggregationParams, + mlAnomaliesEnvironment?: string +) => { + if (!setup.ml) { + return []; + } + + const jobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + mlAnomaliesEnvironment + ); + if (!jobIds.length) { + return []; + } + + const anomalies = await getServiceAnomalies({ + setup, + environment: mlAnomaliesEnvironment, + }); + + return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { + const stats = anomalies.serviceAnomalies[serviceName]; + + const severity = getSeverity(stats.anomalyScore); + const healthStatus = getServiceHealthStatus({ severity }); + + return { + serviceName, + healthStatus, + }; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts new file mode 100644 index 0000000000000..0ee7080dc0834 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + AGENT_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../../common/transaction_types'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + calculateTransactionErrorPercentage, + getOutcomeAggregation, +} from '../../helpers/transaction_error_rate'; +import { ServicesItemsSetup } from './get_services_items'; + +interface AggregationParams { + setup: ServicesItemsSetup; + searchAggregatedTransactions: boolean; +} + +const MAX_NUMBER_OF_SERVICES = 500; + +function calculateAvgDuration({ + value, + deltaAsMinutes, +}: { + value: number; + deltaAsMinutes: number; +}) { + return value / deltaAsMinutes; +} + +export async function getServiceTransactionStats({ + setup, + searchAggregatedTransactions, +}: AggregationParams) { + const { apmEventClient, start, end, esFilter } = setup; + + const outcomes = getOutcomeAggregation({ searchAggregatedTransactions }); + + const metrics = { + real_document_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + avg_duration: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + outcomes, + }; + + const response = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + ...esFilter, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: MAX_NUMBER_OF_SERVICES, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + order: { real_document_count: 'desc' }, + }, + aggs: { + ...metrics, + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + missing: '', + }, + }, + agentName: { + top_hits: { + docvalue_fields: [AGENT_NAME] as const, + size: 1, + }, + }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ + start, + end, + numBuckets: 20, + }).intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: metrics, + }, + }, + }, + }, + }, + }, + }, + }); + + const deltaAsMinutes = (setup.end - setup.start) / 1000 / 60; + + return ( + response.aggregations?.services.buckets.map((bucket) => { + const topTransactionTypeBucket = + bucket.transactionType.buckets.find( + ({ key }) => + key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD + ) ?? bucket.transactionType.buckets[0]; + + return { + serviceName: bucket.key as string, + transactionType: topTransactionTypeBucket.key as string, + environments: topTransactionTypeBucket.environments.buckets + .map((environmentBucket) => environmentBucket.key as string) + .filter(Boolean), + agentName: topTransactionTypeBucket.agentName.hits.hits[0].fields[ + 'agent.name' + ]?.[0] as AgentName, + avgResponseTime: { + value: topTransactionTypeBucket.avg_duration.value, + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.avg_duration.value, + }) + ), + }, + transactionErrorRate: { + value: calculateTransactionErrorPercentage( + topTransactionTypeBucket.outcomes + ), + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: calculateTransactionErrorPercentage(dateBucket.outcomes), + }) + ), + }, + transactionsPerMinute: { + value: calculateAvgDuration({ + value: topTransactionTypeBucket.real_document_count.value, + deltaAsMinutes, + }), + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: calculateAvgDuration({ + value: dateBucket.real_document_count.value, + deltaAsMinutes, + }), + }) + ), + }, + }; + }) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 11f3e44fce87c..359c677b00baf 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -7,14 +7,8 @@ import { Logger } from '@kbn/logging'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { getServicesProjection } from '../../../projections/services'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - getAgentNames, - getEnvironments, - getHealthStatuses, - getTransactionDurationAverages, - getTransactionErrorRates, - getTransactionRates, -} from './get_services_items_stats'; +import { getHealthStatuses } from './get_health_statuses'; +import { getServiceTransactionStats } from './get_service_transaction_stats'; export type ServicesItemsSetup = Setup & SetupTimeRange; export type ServicesItemsProjection = ReturnType; @@ -37,46 +31,23 @@ export async function getServicesItems({ searchAggregatedTransactions, }; - const [ - transactionDurationAverages, - agentNames, - transactionRates, - transactionErrorRates, - environments, - healthStatuses, - ] = await Promise.all([ - getTransactionDurationAverages(params), - getAgentNames(params), - getTransactionRates(params), - getTransactionErrorRates(params), - getEnvironments(params), + const [transactionStats, healthStatuses] = await Promise.all([ + getServiceTransactionStats(params), getHealthStatuses(params, setup.uiFilters.environment).catch((err) => { logger.error(err); return []; }), ]); - const apmServiceMetrics = joinByKey( - [ - ...transactionDurationAverages, - ...agentNames, - ...transactionRates, - ...transactionErrorRates, - ...environments, - ], - 'serviceName' - ); - - const apmServices = apmServiceMetrics.map(({ serviceName }) => serviceName); + const apmServices = transactionStats.map(({ serviceName }) => serviceName); // make sure to exclude health statuses from services // that are not found in APM data - const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => apmServices.includes(serviceName) ); - const allMetrics = [...apmServiceMetrics, ...matchedHealthStatuses]; + const allMetrics = [...transactionStats, ...matchedHealthStatuses]; return joinByKey(allMetrics, 'serviceName'); } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts deleted file mode 100644 index c8ebaa13d9df9..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ /dev/null @@ -1,413 +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 { getServiceHealthStatus } from '../../../../common/service_health_status'; -import { EventOutcome } from '../../../../common/event_outcome'; -import { getSeverity } from '../../../../common/anomaly_detection'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { - AGENT_NAME, - SERVICE_ENVIRONMENT, - EVENT_OUTCOME, -} from '../../../../common/elasticsearch_fieldnames'; -import { mergeProjection } from '../../../projections/util/merge_projection'; -import { - ServicesItemsSetup, - ServicesItemsProjection, -} from './get_services_items'; -import { - getDocumentTypeFilterForAggregatedTransactions, - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { - getMLJobIds, - getServiceAnomalies, -} from '../../service_map/get_service_anomalies'; -import { - calculateTransactionErrorPercentage, - getOutcomeAggregation, - getTransactionErrorRateTimeSeries, -} from '../../helpers/transaction_error_rate'; - -function getDateHistogramOpts(start: number, end: number) { - return { - field: '@timestamp', - fixed_interval: getBucketSize({ start, end, numBuckets: 20 }) - .intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }; -} - -const MAX_NUMBER_OF_SERVICES = 500; - -const getDeltaAsMinutes = (setup: ServicesItemsSetup) => - (setup.end - setup.start) / 1000 / 60; - -interface AggregationParams { - setup: ServicesItemsSetup; - projection: ServicesItemsProjection; - searchAggregatedTransactions: boolean; -} - -export const getTransactionDurationAverages = async ({ - setup, - projection, - searchAggregatedTransactions, -}: AggregationParams) => { - const { apmEventClient, start, end } = setup; - - const response = await apmEventClient.search( - mergeProjection(projection, { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - average: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - timeseries: { - date_histogram: getDateHistogramOpts(start, end), - aggs: { - average: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => ({ - serviceName: serviceBucket.key as string, - avgResponseTime: { - value: serviceBucket.average.value, - timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.average.value, - })), - }, - })); -}; - -export const getAgentNames = async ({ - setup, - projection, -}: AggregationParams) => { - const { apmEventClient } = setup; - const response = await apmEventClient.search( - mergeProjection(projection, { - body: { - size: 0, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - agent_name: { - top_hits: { - _source: [AGENT_NAME], - size: 1, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => ({ - serviceName: serviceBucket.key as string, - agentName: serviceBucket.agent_name.hits.hits[0]?._source.agent - .name as AgentName, - })); -}; - -export const getTransactionRates = async ({ - setup, - projection, - searchAggregatedTransactions, -}: AggregationParams) => { - const { apmEventClient, start, end } = setup; - const response = await apmEventClient.search( - mergeProjection(projection, { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - timeseries: { - date_histogram: getDateHistogramOpts(start, end), - aggs: { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - const deltaAsMinutes = getDeltaAsMinutes(setup); - - return aggregations.services.buckets.map((serviceBucket) => { - const transactionsPerMinute = serviceBucket.count.value / deltaAsMinutes; - return { - serviceName: serviceBucket.key as string, - transactionsPerMinute: { - value: transactionsPerMinute, - timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.count.value / deltaAsMinutes, - })), - }, - }; - }); -}; - -export const getTransactionErrorRates = async ({ - setup, - projection, - searchAggregatedTransactions, -}: AggregationParams) => { - const { apmEventClient, start, end } = setup; - - const outcomes = getOutcomeAggregation({ searchAggregatedTransactions }); - - const response = await apmEventClient.search( - mergeProjection(projection, { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], - }, - }, - ], - }, - }, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - outcomes, - timeseries: { - date_histogram: getDateHistogramOpts(start, end), - aggs: { - outcomes, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => { - const transactionErrorRate = calculateTransactionErrorPercentage( - serviceBucket.outcomes - ); - return { - serviceName: serviceBucket.key as string, - transactionErrorRate: { - value: transactionErrorRate, - timeseries: getTransactionErrorRateTimeSeries( - serviceBucket.timeseries.buckets - ), - }, - }; - }); -}; - -export const getEnvironments = async ({ - setup, - projection, -}: AggregationParams) => { - const { apmEventClient, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - const response = await apmEventClient.search( - mergeProjection(projection, { - body: { - size: 0, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - environments: { - terms: { - field: SERVICE_ENVIRONMENT, - size: maxServiceEnvironments, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => ({ - serviceName: serviceBucket.key as string, - environments: serviceBucket.environments.buckets.map( - (envBucket) => envBucket.key as string - ), - })); -}; - -export const getHealthStatuses = async ( - { setup }: AggregationParams, - mlAnomaliesEnvironment?: string -) => { - if (!setup.ml) { - return []; - } - - const jobIds = await getMLJobIds( - setup.ml.anomalyDetectors, - mlAnomaliesEnvironment - ); - if (!jobIds.length) { - return []; - } - - const anomalies = await getServiceAnomalies({ - setup, - environment: mlAnomaliesEnvironment, - }); - - return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { - const stats = anomalies.serviceAnomalies[serviceName]; - - const severity = getSeverity(stats.anomalyScore); - const healthStatus = getServiceHealthStatus({ severity }); - - return { - serviceName, - healthStatus, - }; - }); -}; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts index e7007f8db0197..be374ccfe3400 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -34,7 +34,7 @@ async function searchThroughput({ intervalString, }: { serviceName: string; - transactionType: string | undefined; + transactionType: string; transactionName: string | undefined; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; @@ -48,6 +48,7 @@ async function searchThroughput({ ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), + { term: { [TRANSACTION_TYPE]: transactionType } }, ...setup.esFilter, ]; @@ -55,10 +56,6 @@ async function searchThroughput({ filter.push({ term: { [TRANSACTION_NAME]: transactionName } }); } - if (transactionType) { - filter.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - const field = getTransactionDurationFieldForAggregatedTransactions( searchAggregatedTransactions ); @@ -104,7 +101,7 @@ export async function getThroughputCharts({ searchAggregatedTransactions, }: { serviceName: string; - transactionType: string | undefined; + transactionType: string; transactionName: string | undefined; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index fdf2fe3521d7e..70755540721dd 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -64,7 +64,7 @@ export const createCustomLinkRoute = createRoute({ params: t.type({ body: payloadRt, }), - options: { tags: ['access:apm'] }, + options: { tags: ['access:apm', 'access:apm_write'] }, handler: async ({ context, request }) => { if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 12a2ea113b1a1..50510fba78512 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -35,9 +35,7 @@ export const transactionGroupsRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ - t.type({ - transactionType: t.string, - }), + t.type({ transactionType: t.string }), uiFiltersRt, rangeRt, ]), @@ -199,10 +197,8 @@ export const transactionThroughputChatsRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ - t.partial({ - transactionType: t.string, - transactionName: t.string, - }), + t.type({ transactionType: t.string }), + t.partial({ transactionName: t.string }), uiFiltersRt, rangeRt, ]), @@ -287,12 +283,8 @@ export const transactionChartsBreakdownRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ - t.type({ - transactionType: t.string, - }), - t.partial({ - transactionName: t.string, - }), + t.type({ transactionType: t.string }), + t.partial({ transactionName: t.string }), uiFiltersRt, rangeRt, ]), @@ -322,10 +314,8 @@ export const transactionChartsErrorRateRoute = createRoute({ query: t.intersection([ uiFiltersRt, rangeRt, - t.partial({ - transactionType: t.string, - transactionName: t.string, - }), + t.type({ transactionType: t.string }), + t.partial({ transactionName: t.string }), ]), }), options: { tags: ['access:apm'] }, diff --git a/x-pack/plugins/beats_management/public/lib/__tests__/config_blocks.test.ts b/x-pack/plugins/beats_management/public/lib/config_blocks.test.ts similarity index 93% rename from x-pack/plugins/beats_management/public/lib/__tests__/config_blocks.test.ts rename to x-pack/plugins/beats_management/public/lib/config_blocks.test.ts index 179df6c19e21d..1a1286e32f1c0 100644 --- a/x-pack/plugins/beats_management/public/lib/__tests__/config_blocks.test.ts +++ b/x-pack/plugins/beats_management/public/lib/config_blocks.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { configBlockSchemas } from '../../../common/config_schemas'; -import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; -import { ConfigBlocksLib } from '../configuration_blocks'; -import { MemoryConfigBlocksAdapter } from './../adapters/configuration_blocks/memory_config_blocks_adapter'; +import { configBlockSchemas } from '../../common/config_schemas'; +import { translateConfigSchema } from '../../common/config_schemas_translations_map'; +import { ConfigBlocksLib } from './configuration_blocks'; +import { MemoryConfigBlocksAdapter } from './adapters/configuration_blocks/memory_config_blocks_adapter'; describe('Tags Client Domain Lib', () => { let lib: ConfigBlocksLib; diff --git a/x-pack/plugins/canvas/__tests__/fixtures/elasticsearch.js b/x-pack/plugins/canvas/__fixtures__/elasticsearch.js similarity index 100% rename from x-pack/plugins/canvas/__tests__/fixtures/elasticsearch.js rename to x-pack/plugins/canvas/__fixtures__/elasticsearch.js diff --git a/x-pack/plugins/canvas/__tests__/fixtures/elasticsearch_plugin.js b/x-pack/plugins/canvas/__fixtures__/elasticsearch_plugin.js similarity index 100% rename from x-pack/plugins/canvas/__tests__/fixtures/elasticsearch_plugin.js rename to x-pack/plugins/canvas/__fixtures__/elasticsearch_plugin.js diff --git a/x-pack/plugins/canvas/__tests__/fixtures/function_specs.ts b/x-pack/plugins/canvas/__fixtures__/function_specs.ts similarity index 63% rename from x-pack/plugins/canvas/__tests__/fixtures/function_specs.ts rename to x-pack/plugins/canvas/__fixtures__/function_specs.ts index 125dd20a66d8a..2d35fdae48985 100644 --- a/x-pack/plugins/canvas/__tests__/fixtures/function_specs.ts +++ b/x-pack/plugins/canvas/__fixtures__/function_specs.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functions as browserFns } from '../../canvas_plugin_src/functions/browser'; -import { ExpressionFunction } from '../../../../../src/plugins/expressions'; -import { initFunctions } from '../../public/functions'; +import { functions as browserFns } from '../canvas_plugin_src/functions/browser'; +import { ExpressionFunction } from '../../../../src/plugins/expressions'; +import { initFunctions } from '../public/functions'; export const functionSpecs = browserFns .concat(...(initFunctions({} as any) as any)) diff --git a/x-pack/plugins/canvas/__tests__/fixtures/kibana.js b/x-pack/plugins/canvas/__fixtures__/kibana.js similarity index 100% rename from x-pack/plugins/canvas/__tests__/fixtures/kibana.js rename to x-pack/plugins/canvas/__fixtures__/kibana.js diff --git a/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/plugins/canvas/__fixtures__/workpads.ts similarity index 99% rename from x-pack/plugins/canvas/__tests__/fixtures/workpads.ts rename to x-pack/plugins/canvas/__fixtures__/workpads.ts index 0d2939ed0e8a5..886d38759d099 100644 --- a/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/plugins/canvas/__fixtures__/workpads.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CanvasWorkpad, CanvasElement, CanvasPage, CanvasVariable } from '../../types'; +import { CanvasWorkpad, CanvasElement, CanvasPage, CanvasVariable } from '../types'; const BaseWorkpad: CanvasWorkpad = { '@created': '2019-02-08T18:35:23.029Z', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/canvas_addons.ts b/x-pack/plugins/canvas/canvas_plugin_src/canvas_addons.ts new file mode 100644 index 0000000000000..fe5e1d7e5f983 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/canvas_addons.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. + */ + +// @ts-expect-error Untyped Local +export * from './uis/datasources'; +export * from './elements'; +// @ts-expect-error Untyped Local +export * from './uis/models'; +export * from './uis/views'; +export * from './uis/arguments'; +export * from './uis/tags'; +// @ts-expect-error Untyped Local +export * from './uis/transforms'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js index 1c75f5b7e0fbc..cf20f9830310f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable } from '../common/__tests__/fixtures/test_tables'; -import { fontStyle } from '../common/__tests__/fixtures/test_styles'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable } from '../common/__fixtures__/test_tables'; +import { fontStyle } from '../common/__fixtures__/test_styles'; import { markdown } from './markdown'; describe('markdown', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_filters.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_filters.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_filters.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_filters.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_pointseries.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_pointseries.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_pointseries.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_pointseries.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_styles.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js similarity index 96% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_styles.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js index 1848d796c61c5..7694cb699d6be 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_styles.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { elasticLogo } from '../../../../lib/elastic_logo'; +import { elasticLogo } from '../../../lib/elastic_logo'; export const fontStyle = { type: 'style', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_tables.ts similarity index 98% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_tables.ts index ffb76500a35d6..f2c325b6d75f9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_tables.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datatable } from '../../../../../types'; +import { Datatable } from '../../../../types'; const emptyTable: Datatable = { type: 'datatable', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/compare.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/compare.js deleted file mode 100644 index 2a255ff742962..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/compare.js +++ /dev/null @@ -1,128 +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 { compare } from '../compare'; -import { functionWrapper } from '../../../../__tests__/helpers/function_wrapper'; -import { getFunctionErrors } from '../../../../i18n'; - -const errors = getFunctionErrors().compare; - -describe('compare', () => { - const fn = functionWrapper(compare); - - describe('args', () => { - describe('op', () => { - it('sets the operator', () => { - expect(fn(0, { op: 'lt', to: 1 })).to.be(true); - }); - - it("defaults to 'eq'", () => { - expect(fn(0, { to: 1 })).to.be(false); - expect(fn(0, { to: 0 })).to.be(true); - }); - - it('throws when invalid op is provided', () => { - expect(() => fn(1, { op: 'boo', to: 2 })).to.throwException( - new RegExp(errors.invalidCompareOperator('boo').message) - ); - expect(() => fn(1, { op: 'boo' })).to.throwException( - new RegExp(errors.invalidCompareOperator('boo').message) - ); - }); - }); - - describe('to', () => { - it('sets the value that context is compared to', () => { - expect(fn(0, { to: 1 })).to.be(false); - }); - - it('if not provided, ne returns true while every other operator returns false', () => { - expect(fn(null, { op: 'ne' })).to.be(true); - expect(fn(0, { op: 'ne' })).to.be(true); - expect(fn(true, { op: 'lte' })).to.be(false); - expect(fn(1, { op: 'gte' })).to.be(false); - expect(fn('foo', { op: 'lt' })).to.be(false); - expect(fn(null, { op: 'gt' })).to.be(false); - expect(fn(null, { op: 'eq' })).to.be(false); - }); - }); - }); - - describe('same type comparisons', () => { - describe('null', () => { - it('returns true', () => { - expect(fn(null, { op: 'eq', to: null })).to.be(true); - expect(fn(null, { op: 'lte', to: null })).to.be(true); - expect(fn(null, { op: 'gte', to: null })).to.be(true); - }); - - it('returns false', () => { - expect(fn(null, { op: 'ne', to: null })).to.be(false); - expect(fn(null, { op: 'lt', to: null })).to.be(false); - expect(fn(null, { op: 'gt', to: null })).to.be(false); - }); - }); - - describe('number', () => { - it('returns true', () => { - expect(fn(-2.34, { op: 'lt', to: 10 })).to.be(true); - expect(fn(2, { op: 'gte', to: 2 })).to.be(true); - }); - - it('returns false', () => { - expect(fn(2, { op: 'eq', to: 10 })).to.be(false); - expect(fn(10, { op: 'ne', to: 10 })).to.be(false); - expect(fn(1, { op: 'lte', to: -3 })).to.be(false); - expect(fn(2, { op: 'gt', to: 2 })).to.be(false); - }); - }); - - describe('string', () => { - it('returns true', () => { - expect(fn('foo', { op: 'gte', to: 'foo' })).to.be(true); - expect(fn('foo', { op: 'lte', to: 'foo' })).to.be(true); - expect(fn('bar', { op: 'lt', to: 'foo' })).to.be(true); - }); - - it('returns false', () => { - expect(fn('foo', { op: 'eq', to: 'bar' })).to.be(false); - expect(fn('foo', { op: 'ne', to: 'foo' })).to.be(false); - expect(fn('foo', { op: 'gt', to: 'foo' })).to.be(false); - }); - }); - - describe('boolean', () => { - it('returns true', () => { - expect(fn(true, { op: 'eq', to: true })).to.be(true); - expect(fn(false, { op: 'eq', to: false })).to.be(true); - expect(fn(true, { op: 'ne', to: false })).to.be(true); - expect(fn(false, { op: 'ne', to: true })).to.be(true); - }); - it('returns false', () => { - expect(fn(true, { op: 'eq', to: false })).to.be(false); - expect(fn(false, { op: 'eq', to: true })).to.be(false); - expect(fn(true, { op: 'ne', to: true })).to.be(false); - expect(fn(false, { op: 'ne', to: false })).to.be(false); - }); - }); - }); - - describe('different type comparisons', () => { - it("returns true for 'ne' only", () => { - expect(fn(0, { op: 'ne', to: '0' })).to.be(true); - }); - - it('otherwise always returns false', () => { - expect(fn(0, { op: 'eq', to: '0' })).to.be(false); - expect(fn('foo', { op: 'lt', to: 10 })).to.be(false); - expect(fn('foo', { op: 'lte', to: true })).to.be(false); - expect(fn(0, { op: 'gte', to: null })).to.be(false); - expect(fn(0, { op: 'eq', to: false })).to.be(false); - expect(fn(true, { op: 'gte', to: null })).to.be(false); - }); - }); -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/all.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/all.test.js index 63a3facc833d3..697bc2bb10e5d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/all.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/all.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { all } from './all'; describe('all', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js index a8c01f0b2791f..1b4141c6e2cb0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; -import { emptyTable, testTable } from './__tests__/fixtures/test_tables'; +import { emptyTable, testTable } from './__fixtures__/test_tables'; import { alterColumn } from './alterColumn'; const errors = getFunctionErrors().alterColumn; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/any.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/any.test.js index dcff9b299832c..ed411b162685b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/any.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/any.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { any } from './any'; describe('any', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js index 49d14622e80f0..8da92e5b6720c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { asFn } from './as'; describe('as', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/axis_config.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/axis_config.test.js index 04552b988e561..a58c52358f648 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/axis_config.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/axis_config.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; -import { testTable } from './__tests__/fixtures/test_tables'; +import { testTable } from './__fixtures__/test_tables'; import { axisConfig } from './axisConfig'; const errors = getFunctionErrors().axisConfig; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js index 41ec526d1b853..f1ddf88388eac 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { caseFn } from './case'; describe('case', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/clear.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/clear.test.js index b89cbc976a63f..a4177d1fe05ba 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/clear.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/clear.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable } from './__fixtures__/test_tables'; import { clear } from './clear'; describe('clear', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js index e2da579e5e0a3..8e0ae1b7a4fd5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { emptyTable, testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { emptyTable, testTable } from './__fixtures__/test_tables'; import { columns } from './columns'; describe('columns', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.test.js new file mode 100644 index 0000000000000..ddc20ccf7be75 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.test.js @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { getFunctionErrors } from '../../../i18n'; +import { compare } from './compare'; + +const errors = getFunctionErrors().compare; + +describe('compare', () => { + const fn = functionWrapper(compare); + + describe('args', () => { + describe('op', () => { + it('sets the operator', () => { + expect(fn(0, { op: 'lt', to: 1 })).toBe(true); + }); + + it("defaults to 'eq'", () => { + expect(fn(0, { to: 1 })).toBe(false); + expect(fn(0, { to: 0 })).toBe(true); + }); + + it('throws when invalid op is provided', () => { + expect(() => fn(1, { op: 'boo', to: 2 })).toThrowError( + new RegExp(errors.invalidCompareOperator('boo').message) + ); + expect(() => fn(1, { op: 'boo' })).toThrowError( + new RegExp(errors.invalidCompareOperator('boo').message) + ); + }); + }); + + describe('to', () => { + it('sets the value that context is compared to', () => { + expect(fn(0, { to: 1 })).toBe(false); + }); + + it('if not provided, ne returns true while every other operator returns false', () => { + expect(fn(null, { op: 'ne' })).toBe(true); + expect(fn(0, { op: 'ne' })).toBe(true); + expect(fn(true, { op: 'lte' })).toBe(false); + expect(fn(1, { op: 'gte' })).toBe(false); + expect(fn('foo', { op: 'lt' })).toBe(false); + expect(fn(null, { op: 'gt' })).toBe(false); + expect(fn(null, { op: 'eq' })).toBe(false); + }); + }); + }); + + describe('same type comparisons', () => { + describe('null', () => { + it('returns true', () => { + expect(fn(null, { op: 'eq', to: null })).toBe(true); + expect(fn(null, { op: 'lte', to: null })).toBe(true); + expect(fn(null, { op: 'gte', to: null })).toBe(true); + }); + + it('returns false', () => { + expect(fn(null, { op: 'ne', to: null })).toBe(false); + expect(fn(null, { op: 'lt', to: null })).toBe(false); + expect(fn(null, { op: 'gt', to: null })).toBe(false); + }); + }); + + describe('number', () => { + it('returns true', () => { + expect(fn(-2.34, { op: 'lt', to: 10 })).toBe(true); + expect(fn(2, { op: 'gte', to: 2 })).toBe(true); + }); + + it('returns false', () => { + expect(fn(2, { op: 'eq', to: 10 })).toBe(false); + expect(fn(10, { op: 'ne', to: 10 })).toBe(false); + expect(fn(1, { op: 'lte', to: -3 })).toBe(false); + expect(fn(2, { op: 'gt', to: 2 })).toBe(false); + }); + }); + + describe('string', () => { + it('returns true', () => { + expect(fn('foo', { op: 'gte', to: 'foo' })).toBe(true); + expect(fn('foo', { op: 'lte', to: 'foo' })).toBe(true); + expect(fn('bar', { op: 'lt', to: 'foo' })).toBe(true); + }); + + it('returns false', () => { + expect(fn('foo', { op: 'eq', to: 'bar' })).toBe(false); + expect(fn('foo', { op: 'ne', to: 'foo' })).toBe(false); + expect(fn('foo', { op: 'gt', to: 'foo' })).toBe(false); + }); + }); + + describe('boolean', () => { + it('returns true', () => { + expect(fn(true, { op: 'eq', to: true })).toBe(true); + expect(fn(false, { op: 'eq', to: false })).toBe(true); + expect(fn(true, { op: 'ne', to: false })).toBe(true); + expect(fn(false, { op: 'ne', to: true })).toBe(true); + }); + it('returns false', () => { + expect(fn(true, { op: 'eq', to: false })).toBe(false); + expect(fn(false, { op: 'eq', to: true })).toBe(false); + expect(fn(true, { op: 'ne', to: true })).toBe(false); + expect(fn(false, { op: 'ne', to: false })).toBe(false); + }); + }); + }); + + describe('different type comparisons', () => { + it("returns true for 'ne' only", () => { + expect(fn(0, { op: 'ne', to: '0' })).toBe(true); + }); + + it('otherwise always returns false', () => { + expect(fn(0, { op: 'eq', to: '0' })).toBe(false); + expect(fn('foo', { op: 'lt', to: 10 })).toBe(false); + expect(fn('foo', { op: 'lte', to: true })).toBe(false); + expect(fn(0, { op: 'gte', to: null })).toBe(false); + expect(fn(0, { op: 'eq', to: false })).toBe(false); + expect(fn(true, { op: 'gte', to: null })).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js index 3edae257ce69a..574ac15573b4b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { elasticLogo } from '../../lib/elastic_logo'; import { getFunctionErrors } from '../../../i18n'; import { containerStyle } from './containerStyle'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/context.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/context.test.js index 904406f1a703b..32b00bddd4607 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/context.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/context.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable, emptyTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable, emptyTable } from './__fixtures__/test_tables'; import { context } from './context'; describe('context', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.js index 8cd30e3b1915e..4083187c75aae 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; import { csv } from './csv'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/date.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/date.test.js index 96b122d8a0776..4cf892b41dc35 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/date.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/date.test.js @@ -5,7 +5,7 @@ */ import sinon from 'sinon'; -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; import { date } from './date'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/do.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/do.test.js index 46e0e22f53a6d..43310b68f1851 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/do.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/do.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { doFn } from './do'; describe('do', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.js index 3ce44948cd6d0..7eaf005cf1409 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable } from './__fixtures__/test_tables'; import { dropdownControl } from './dropdownControl'; describe('dropdownControl', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/eq.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/eq.test.js index cfe4dc3c9ba82..a890b67dfe1ef 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/eq.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/eq.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { eq } from './eq'; describe('eq', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js index 2b9bdb59afbdf..409570da756ca 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { emptyFilter } from './__tests__/fixtures/test_filters'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { emptyFilter } from './__fixtures__/test_filters'; import { exactly } from './exactly'; describe('exactly', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js index 179be8aff2e19..c0d3ce658d0d1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable } from './__fixtures__/test_tables'; import { filterrows } from './filterrows'; const inStock = (datatable) => datatable.rows[0].in_stock; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatdate.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatdate.test.js index 968727f4aa19d..27a793620d0b5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatdate.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatdate.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { formatdate } from './formatdate'; describe('formatdate', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.test.js index adab2d4075550..5f751be4bb6c7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { formatnumber } from './formatnumber'; describe('formatnumber', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/getCell.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/getCell.test.js index 976681f98260f..c23221298ab9f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/getCell.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/getCell.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; -import { emptyTable, testTable } from './__tests__/fixtures/test_tables'; +import { emptyTable, testTable } from './__fixtures__/test_tables'; import { getCell } from './getCell'; const errors = getFunctionErrors().getCell; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gt.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gt.test.js index 6c8c8de4f5763..71c27bd9e701b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gt.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gt.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { gt } from './gt'; describe('gt', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gte.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gte.test.js index b5f31dbcba2ba..2e996d61da391 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gte.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/gte.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { gte } from './gte'; describe('gte', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/head.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/head.test.js index bfbd548cdce01..74f9252878851 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/head.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/head.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { emptyTable, testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { emptyTable, testTable } from './__fixtures__/test_tables'; import { head } from './head'; describe('head', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js index 408d02ea175f1..6e1a6b4e225a6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { ifFn } from './if'; describe('if', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/image.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js similarity index 84% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/image.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js index 0d1c80665f570..993d38b5e108e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/image.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js @@ -5,13 +5,15 @@ */ import expect from '@kbn/expect'; -import { image } from '../image'; -import { functionWrapper } from '../../../../__tests__/helpers/function_wrapper'; -import { elasticLogo } from '../../../lib/elastic_logo'; -import { elasticOutline } from '../../../lib/elastic_outline'; +// import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { elasticLogo } from '../../lib/elastic_logo'; +import { elasticOutline } from '../../lib/elastic_outline'; +// import { image } from './image'; -describe('image', () => { - const fn = functionWrapper(image); +// TODO: the test was not running and is not up to date +describe.skip('image', () => { + // const fn = functionWrapper(image); + const fn = jest.fn(); it('returns an image object using a dataUrl', () => { const result = fn(null, { dataurl: elasticOutline, mode: 'cover' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/join_rows.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/join_rows.test.js index 5c778300133a0..7479fb1763b9e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/join_rows.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/join_rows.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; -import { testTable } from './__tests__/fixtures/test_tables'; +import { testTable } from './__fixtures__/test_tables'; import { joinRows } from './join_rows'; const errors = getFunctionErrors().joinRows; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lt.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lt.test.js index ba1af11e5b92d..4a34e496bdfe2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lt.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lt.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { lt } from './lt'; describe('lt', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lte.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lte.test.js index d4ebcb5417f22..9aa12d321532e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lte.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/lte.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { lte } from './lte'; describe('lte', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js index 652d61fd77398..cc360a48c7f56 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable, emptyTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable, emptyTable } from './__fixtures__/test_tables'; import { mapColumn } from './mapColumn'; const pricePlusTwo = (datatable) => Promise.resolve(datatable.rows[0].price + 2); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.test.js index 530a0043a7ef1..ff0819ad3ee55 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; -import { emptyTable, testTable } from './__tests__/fixtures/test_tables'; +import { emptyTable, testTable } from './__fixtures__/test_tables'; import { math } from './math'; const errors = getFunctionErrors().math; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js index 50a952d836251..f88e04fafc9db 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { fontStyle } from './__tests__/fixtures/test_styles'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { fontStyle } from './__fixtures__/test_styles'; import { metric } from './metric'; describe('metric', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.test.js index d1083131cfa2f..ecc5fce0300e3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { neq } from './neq'; describe('neq', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js index 07d436007c816..3651cd84873d2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; -import { testTable } from './__tests__/fixtures/test_tables'; +import { testTable } from './__fixtures__/test_tables'; import { ply } from './ply'; const errors = getFunctionErrors().ply; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/progress.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js similarity index 93% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/progress.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js index 17f9defa15dc3..1558f26e26201 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/progress.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js @@ -5,14 +5,15 @@ */ import expect from '@kbn/expect'; -import { progress } from '../progress'; -import { functionWrapper } from '../../../../__tests__/helpers/function_wrapper'; -import { getFunctionErrors } from '../../../../i18n'; -import { fontStyle } from './fixtures/test_styles'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { getFunctionErrors } from '../../../i18n'; +import { progress } from './progress'; +import { fontStyle } from './__fixtures__/test_styles'; const errors = getFunctionErrors().progress; -describe('progress', () => { +// TODO: this test was not running and is not up to date +describe.skip('progress', () => { const fn = functionWrapper(progress); const value = 0.33; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js index 152d7fb4df7ec..ddc4fcee08296 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { DEFAULT_ELEMENT_CSS } from '../../../common/lib/constants'; -import { testTable } from './__tests__/fixtures/test_tables'; -import { fontStyle, containerStyle } from './__tests__/fixtures/test_styles'; +import { testTable } from './__fixtures__/test_tables'; +import { fontStyle, containerStyle } from './__fixtures__/test_styles'; import { render } from './render'; const renderTable = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js index f7c1ecc94a240..f03e8f127e3a2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { elasticOutline } from '../../lib/elastic_outline'; import { elasticLogo } from '../../lib/elastic_logo'; import { repeatImage } from './repeat_image'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/replace.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/replace.test.js index e4a371ad82e1c..853127768604f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/replace.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/replace.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { replace } from './replace'; describe('replace', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/reveal_image.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/reveal_image.test.js index 2efc91e93ddc2..7052fc81b62e0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/reveal_image.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/reveal_image.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { elasticOutline } from '../../lib/elastic_outline'; import { elasticLogo } from '../../lib/elastic_logo'; import { getFunctionErrors } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js index eed8b0aa6c6bf..651117b9bb818 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { rounddate } from './rounddate'; describe('rounddate', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rowCount.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rowCount.test.js index 798ed3a749ad4..52a9b56a4cc4c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rowCount.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rowCount.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { emptyTable, testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { emptyTable, testTable } from './__fixtures__/test_tables'; import { rowCount } from './rowCount'; describe('rowCount', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/series_style.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/series_style.test.js index da2183f6dfbac..236a6cdc25a03 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/series_style.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/series_style.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { seriesStyle } from './seriesStyle'; describe('seriesStyle', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/sort.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/sort.test.js index 88eb31da2552c..37d42f0e7b3d3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/sort.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/sort.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable } from './__fixtures__/test_tables'; import { sort } from './sort'; describe('sort', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js index d137ce05ccc19..c4ba9f200e9b4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable, emptyTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable, emptyTable } from './__fixtures__/test_tables'; import { staticColumn } from './staticColumn'; describe('staticColumn', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/string.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/string.test.js index fbb7ab1b72eed..1250c5228d505 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/string.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/string.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { string } from './string'; describe('string', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js index 7ecccdd5ee544..5c71e101c8262 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { switchFn } from './switch'; describe('switch', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/table.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/table.test.js index c34c437e332ae..6ba450d94c557 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/table.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/table.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { testTable } from './__tests__/fixtures/test_tables'; -import { fontStyle } from './__tests__/fixtures/test_styles'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { testTable } from './__fixtures__/test_tables'; +import { fontStyle } from './__fixtures__/test_styles'; import { table } from './table'; describe('table', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js index 45d6cba46716a..a95ac31b00079 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { emptyTable, testTable } from './__tests__/fixtures/test_tables'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; +import { emptyTable, testTable } from './__fixtures__/test_tables'; import { tail } from './tail'; describe('tail', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js index 2edbba278ffde..d650a6ca792e8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js @@ -5,9 +5,9 @@ */ import sinon from 'sinon'; -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; -import { emptyFilter } from './__tests__/fixtures/test_filters'; +import { emptyFilter } from './__fixtures__/test_filters'; import { timefilter } from './timefilter'; const errors = getFunctionErrors().timefilter; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter_control.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter_control.test.js index fa7b26554f65b..edc57994c7d57 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter_control.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter_control.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; +import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { timefilterControl } from './timefilterControl'; describe('timefilterControl', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js index 237e80f0c1ee3..0dbc7ca833f05 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { emptyTable, testTable } from '../common/__tests__/fixtures/test_tables'; +import { emptyTable, testTable } from '../common/__fixtures__/test_tables'; import { getExpressionType } from './pointseries/lib/get_expression_type'; describe('getExpressionType', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries.test.js index 2029388115bcc..1adb7bc53987d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { emptyTable, testTable } from '../common/__tests__/fixtures/test_tables'; +import { emptyTable, testTable } from '../common/__fixtures__/test_tables'; import { pointseries } from './pointseries'; describe('pointseries', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index 7ecebd6d0677a..e650cd5037118 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -14,17 +14,6 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; -import { initializeElements } from './elements'; -// @ts-expect-error untyped local -import { transformSpecs } from './uis/transforms'; -// @ts-expect-error untyped local -import { datasourceSpecs } from './uis/datasources'; -// @ts-expect-error untyped local -import { modelSpecs } from './uis/models'; -import { initializeViews } from './uis/views'; -import { initializeArgs } from './uis/arguments'; -import { tagSpecs } from './uis/tags'; - interface SetupDeps { canvas: CanvasSetup; } @@ -53,13 +42,44 @@ export class CanvasSrcPlugin implements Plugin ); }); - plugins.canvas.addElements(initializeElements(core, plugins)); - plugins.canvas.addDatasourceUIs(datasourceSpecs); - plugins.canvas.addModelUIs(modelSpecs); - plugins.canvas.addViewUIs(initializeViews(core, plugins)); - plugins.canvas.addArgumentUIs(initializeArgs(core, plugins)); - plugins.canvas.addTagUIs(tagSpecs); - plugins.canvas.addTransformUIs(transformSpecs); + plugins.canvas.addDatasourceUIs(async () => { + // @ts-expect-error + const { datasourceSpecs } = await import('./canvas_addons'); + return datasourceSpecs; + }); + + plugins.canvas.addElements(async () => { + const { initializeElements } = await import('./canvas_addons'); + return initializeElements(core, plugins); + }); + + plugins.canvas.addModelUIs(async () => { + // @ts-expect-error Untyped local + const { modelSpecs } = await import('./canvas_addons'); + return modelSpecs; + }); + + plugins.canvas.addViewUIs(async () => { + const { initializeViews } = await import('./canvas_addons'); + + return initializeViews(core, plugins); + }); + + plugins.canvas.addArgumentUIs(async () => { + const { initializeArgs } = await import('./canvas_addons'); + return initializeArgs(core, plugins); + }); + + plugins.canvas.addTagUIs(async () => { + const { tagSpecs } = await import('./canvas_addons'); + return tagSpecs; + }); + + plugins.canvas.addTransformUIs(async () => { + // @ts-expect-error Untyped local + const { transformSpecs } = await import('./canvas_addons'); + return transformSpecs; + }); } public start(core: CoreStart, plugins: StartDeps) {} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx index 54702f2654839..34f4bb39fbfa7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -12,6 +12,7 @@ export const defaultHandlers: RendererHandlers = { getElementId: () => 'element-id', getFilter: () => 'filter', getRenderMode: () => 'display', + isSyncColorsEnabled: () => false, onComplete: (fn) => undefined, onEmbeddableDestroyed: action('onEmbeddableDestroyed'), onEmbeddableInputChange: action('onEmbeddableInputChange'), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/__tests__/get_form_object.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/__tests__/get_form_object.js deleted file mode 100644 index cb8e999489fbf..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/__tests__/get_form_object.js +++ /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 expect from '@kbn/expect'; -import { getFormObject } from '../get_form_object'; - -describe('getFormObject', () => { - describe('valid input', () => { - it('string', () => { - expect(getFormObject('field')).to.be.eql({ fn: '', column: 'field' }); - }); - it('simple expression', () => { - expect(getFormObject('mean(field)')).to.be.eql({ fn: 'mean', column: 'field' }); - }); - }); - describe('invalid input', () => { - it('number', () => { - expect(getFormObject) - .withArgs('2') - .to.throwException((e) => { - expect(e.message).to.be('Cannot render scalar values or complex math expressions'); - }); - }); - it('complex expression', () => { - expect(getFormObject) - .withArgs('mean(field * 3)') - .to.throwException((e) => { - expect(e.message).to.be('Cannot render scalar values or complex math expressions'); - }); - }); - }); -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.js new file mode 100644 index 0000000000000..9458f0fd095d3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getFormObject } from './get_form_object'; + +describe('getFormObject', () => { + describe('valid input', () => { + it('string', () => { + expect(getFormObject('field')).toEqual({ fn: '', column: 'field' }); + }); + it('simple expression', () => { + expect(getFormObject('mean(field)')).toEqual({ fn: 'mean', column: 'field' }); + }); + }); + describe('invalid input', () => { + it('number', () => { + expect(() => { + getFormObject('2'); + }).toThrow('Cannot render scalar values or complex math expressions'); + }); + it('complex expression', () => { + expect(() => { + getFormObject('mean(field * 3)'); + }).toThrow('Cannot render scalar values or complex math expressions'); + }); + }); +}); diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts index 777810cad05ba..128fb6795f854 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts +++ b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionSpecs } from '../../__tests__/fixtures/function_specs'; +import { functionSpecs } from '../../__fixtures__/function_specs'; import { FunctionSuggestion, @@ -17,7 +17,7 @@ describe('autocomplete', () => { it('should return function definition for plot', () => { const expression = 'plot '; const def = getFnArgDefAtPosition(functionSpecs, expression, expression.length); - const plotFn = functionSpecs.find((spec) => spec.name === 'plot'); + const plotFn = functionSpecs.find((spec: any) => spec.name === 'plot'); expect(def.fnDef).toBe(plotFn); }); }); @@ -33,7 +33,7 @@ describe('autocomplete', () => { it('should suggest arguments', () => { const expression = 'plot '; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); - const plotFn = functionSpecs.find((spec) => spec.name === 'plot'); + const plotFn = functionSpecs.find((spec: any) => spec.name === 'plot'); expect(suggestions.length).toBe(Object.keys(plotFn!.args).length); expect(suggestions[0].start).toBe(expression.length); expect(suggestions[0].end).toBe(expression.length); @@ -42,7 +42,7 @@ describe('autocomplete', () => { it('should suggest values', () => { const expression = 'shape shape='; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); - const shapeFn = functionSpecs.find((spec) => spec.name === 'shape'); + const shapeFn = functionSpecs.find((spec: any) => spec.name === 'shape'); expect(suggestions.length).toBe(shapeFn!.args.shape.options.length); expect(suggestions[0].start).toBe(expression.length); expect(suggestions[0].end).toBe(expression.length); @@ -110,7 +110,7 @@ describe('autocomplete', () => { expression, expression.length - 1 ); - const ltFn = functionSpecs.find((spec) => spec.name === 'lt'); + const ltFn = functionSpecs.find((spec: any) => spec.name === 'lt'); expect(suggestions.length).toBe(Object.keys(ltFn!.args).length); expect(suggestions[0].start).toBe(expression.length - 1); expect(suggestions[0].end).toBe(expression.length - 1); @@ -123,7 +123,7 @@ describe('autocomplete', () => { expression, expression.length - 1 ); - const shapeFn = functionSpecs.find((spec) => spec.name === 'shape'); + const shapeFn = functionSpecs.find((spec: any) => spec.name === 'shape'); expect(suggestions.length).toBe(shapeFn!.args.shape.options.length); expect(suggestions[0].start).toBe(expression.length - 1); expect(suggestions[0].end).toBe(expression.length - 1); @@ -136,7 +136,7 @@ describe('autocomplete', () => { expression, expression.length - 1 ); - const shapeFn = functionSpecs.find((spec) => spec.name === 'shape'); + const shapeFn = functionSpecs.find((spec: any) => spec.name === 'shape'); expect(suggestions.length).toBe(shapeFn!.args.shape.options.length); expect(suggestions[0].start).toBe(expression.length - '"ar"'.length); expect(suggestions[0].end).toBe(expression.length); diff --git a/x-pack/plugins/canvas/common/lib/get_field_type.test.ts b/x-pack/plugins/canvas/common/lib/get_field_type.test.ts index 82e724c33ecc8..3e8f99750671c 100644 --- a/x-pack/plugins/canvas/common/lib/get_field_type.test.ts +++ b/x-pack/plugins/canvas/common/lib/get_field_type.test.ts @@ -7,7 +7,7 @@ import { emptyTable, testTable, -} from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_tables'; +} from '../../canvas_plugin_src/functions/common/__fixtures__/test_tables'; import { getFieldType } from './get_field_type'; describe('getFieldType', () => { diff --git a/x-pack/plugins/canvas/common/lib/handlebars.test.js b/x-pack/plugins/canvas/common/lib/handlebars.test.js index 5fcb2d42395fa..20574279bad96 100644 --- a/x-pack/plugins/canvas/common/lib/handlebars.test.js +++ b/x-pack/plugins/canvas/common/lib/handlebars.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { testTable } from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_tables'; +import { testTable } from '../../canvas_plugin_src/functions/common/__fixtures__/test_tables'; import { Handlebars } from './handlebars'; describe('handlebars', () => { diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 7d65a99b1dd45..fc02df3740cdb 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -103,7 +103,7 @@ export const initializeCanvas = async ( // Init Registries initRegistries(); - populateRegistries(registries); + await populateRegistries(registries); // Set Badge coreStart.chrome.setBadge( diff --git a/x-pack/plugins/canvas/public/apps/export/export/__tests__/__snapshots__/export_app.test.tsx.snap b/x-pack/plugins/canvas/public/apps/export/export/__snapshots__/export_app.test.tsx.snap similarity index 100% rename from x-pack/plugins/canvas/public/apps/export/export/__tests__/__snapshots__/export_app.test.tsx.snap rename to x-pack/plugins/canvas/public/apps/export/export/__snapshots__/export_app.test.tsx.snap diff --git a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx b/x-pack/plugins/canvas/public/apps/export/export/export_app.test.tsx similarity index 84% rename from x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx rename to x-pack/plugins/canvas/public/apps/export/export/export_app.test.tsx index 1bb58919b7fa6..6a483b23e8e98 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx +++ b/x-pack/plugins/canvas/public/apps/export/export/export_app.test.tsx @@ -6,18 +6,18 @@ import React from 'react'; import { mount } from 'enzyme'; -import { ExportApp } from '../export_app.component'; -import { CanvasWorkpad } from '../../../../../types'; +import { ExportApp } from './export_app.component'; +import { CanvasWorkpad } from '../../../../types'; jest.mock('style-it', () => ({ it: (css: string, Component: any) => Component, })); -jest.mock('../../../../components/workpad_page', () => ({ +jest.mock('../../../components/workpad_page', () => ({ WorkpadPage: (props: any) =>
Page
, })); -jest.mock('../../../../components/link', () => ({ +jest.mock('../../../components/link', () => ({ Link: (props: any) =>
Link
, })); diff --git a/x-pack/plugins/canvas/public/components/download/__tests__/download.test.tsx b/x-pack/plugins/canvas/public/components/download/download.test.tsx similarity index 95% rename from x-pack/plugins/canvas/public/components/download/__tests__/download.test.tsx rename to x-pack/plugins/canvas/public/components/download/download.test.tsx index 3bbe8193deeea..a4ea446e9fce3 100644 --- a/x-pack/plugins/canvas/public/components/download/__tests__/download.test.tsx +++ b/x-pack/plugins/canvas/public/components/download/download.test.tsx @@ -6,7 +6,7 @@ import { render } from 'enzyme'; import React from 'react'; -import { Download } from '..'; +import { Download } from '.'; describe('', () => { test('has canvasDownload class', () => { diff --git a/x-pack/plugins/canvas/public/components/expression/expression.js b/x-pack/plugins/canvas/public/components/expression/expression.tsx similarity index 86% rename from x-pack/plugins/canvas/public/components/expression/expression.js rename to x-pack/plugins/canvas/public/components/expression/expression.tsx index 37cf1b821d9fd..141963d479724 100644 --- a/x-pack/plugins/canvas/public/components/expression/expression.js +++ b/x-pack/plugins/canvas/public/components/expression/expression.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC, MutableRefObject } from 'react'; import PropTypes from 'prop-types'; import { EuiPanel, @@ -16,19 +16,26 @@ import { EuiLink, EuiPortal, } from '@elastic/eui'; +// @ts-expect-error import { Shortcuts } from 'react-shortcuts'; import { ComponentStrings } from '../../../i18n'; import { ExpressionInput } from '../expression_input'; import { ToolTipShortcut } from '../tool_tip_shortcut'; +import { ExpressionFunction } from '../../../types'; +import { FormState } from './'; const { Expression: strings } = ComponentStrings; const { useRef } = React; -const shortcut = (ref, cmd, callback) => ( +const shortcut = ( + ref: MutableRefObject, + cmd: string, + callback: () => void +) => ( { + handler={(command: string) => { const isInputActive = ref.current && ref.current.editor && ref.current.editor.hasTextFocus(); if (isInputActive && command === cmd) { callback(); @@ -40,18 +47,28 @@ const shortcut = (ref, cmd, callback) => ( /> ); -export const Expression = ({ +interface Props { + functionDefinitions: ExpressionFunction[]; + formState: FormState; + updateValue: (expression?: string) => void; + setExpression: (expression: string) => void; + done: () => void; + error?: string; + isCompact: boolean; + toggleCompactView: () => void; +} + +export const Expression: FC = ({ functionDefinitions, formState, updateValue, setExpression, done, error, - fontSize, isCompact, toggleCompactView, }) => { - const refExpressionInput = useRef(null); + const refExpressionInput = useRef(null); const handleRun = () => { setExpression(formState.expression); @@ -78,7 +95,6 @@ export const Expression = ({ ({ - pageId: getSelectedPage(state), - element: getSelectedElement(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - setExpression: (elementId, pageId) => (expression) => { - // destroy the context cache - dispatch(flushContext(elementId)); - - // update the element's expression - dispatch(setExpression(expression, elementId, pageId)); - }, -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { element, pageId } = stateProps; - const allProps = { ...ownProps, ...stateProps, ...dispatchProps }; - - if (!element) { - return allProps; - } - - const { expression } = element; - - const functions = Object.values(allProps.services.expressions.getFunctions()); - - return { - ...allProps, - expression, - functionDefinitions: functions, - setExpression: dispatchProps.setExpression(element.id, pageId), - }; -}; - -const expressionLifecycle = lifecycle({ - componentDidUpdate({ expression }) { - if ( - this.props.expression !== expression && - this.props.expression !== this.props.formState.expression - ) { - this.props.setFormState({ - expression: this.props.expression, - dirty: false, - }); - } - }, -}); - -export const Expression = compose( - withServices, - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withState('formState', 'setFormState', ({ expression }) => ({ - expression, - dirty: false, - })), - withState('isCompact', 'setCompact', true), - withHandlers({ - toggleCompactView: ({ isCompact, setCompact }) => () => { - setCompact(!isCompact); - }, - updateValue: ({ setFormState }) => (expression) => { - setFormState({ - expression, - dirty: true, - }); - }, - setExpression: ({ setExpression, setFormState }) => (exp) => { - setFormState((prev) => ({ - ...prev, - dirty: false, - })); - setExpression(exp); - }, - }), - expressionLifecycle, - withPropsOnChange(['formState'], ({ formState }) => ({ - error: (function () { - try { - // TODO: We should merge the advanced UI input and this into a single validated expression input. - fromExpression(formState.expression); - return null; - } catch (e) { - return e.message; - } - })(), - })), - branch((props) => !props.element, renderComponent(ElementNotSelected)) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/expression/index.tsx b/x-pack/plugins/canvas/public/components/expression/index.tsx new file mode 100644 index 0000000000000..fc4f1958ecb33 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/expression/index.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 React, { FC, useState, useCallback, useMemo, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fromExpression } from '@kbn/interpreter/common'; +import { useServices } from '../../services'; +import { getSelectedPage, getSelectedElement } from '../../state/selectors/workpad'; +// @ts-expect-error +import { setExpression, flushContext } from '../../state/actions/elements'; +// @ts-expect-error +import { ElementNotSelected } from './element_not_selected'; +import { Expression as Component } from './expression'; +import { State, CanvasElement } from '../../../types'; + +interface ExpressionProps { + done: () => void; +} + +interface ExpressionContainerProps extends ExpressionProps { + element: CanvasElement; + pageId: string; +} + +export interface FormState { + dirty: boolean; + expression: string; +} + +export const Expression: FC = ({ done }) => { + const { element, pageId } = useSelector((state: State) => ({ + pageId: getSelectedPage(state), + element: getSelectedElement(state), + })); + + if (!element) { + return ; + } + + return ; +}; + +const ExpressionContainer: FC = ({ done, element, pageId }) => { + const services = useServices(); + const dispatch = useDispatch(); + const [isCompact, setCompact] = useState(true); + const toggleCompactView = useCallback(() => { + setCompact(!isCompact); + }, [isCompact, setCompact]); + + const dispatchSetExpression = useCallback( + (expression: string) => { + // destroy the context cache + dispatch(flushContext(element.id)); + + // update the element's expression + dispatch(setExpression(expression, element.id, pageId)); + }, + [dispatch, element, pageId] + ); + + const [formState, setFormState] = useState({ + dirty: false, + expression: element.expression, + }); + + const updateValue = useCallback( + (expression: string = '') => { + setFormState({ + expression, + dirty: true, + }); + }, + [setFormState] + ); + + const onSetExpression = useCallback( + (expression: string) => { + setFormState({ + ...formState, + dirty: false, + }); + dispatchSetExpression(expression); + }, + [setFormState, dispatchSetExpression, formState] + ); + + const currentExpression = formState.expression; + + const error = useMemo(() => { + try { + // TODO: We should merge the advanced UI input and this into a single validated expression input. + fromExpression(currentExpression); + return null; + } catch (e) { + return e.message; + } + }, [currentExpression]); + + useEffect(() => { + if (element.expression !== formState.expression && !formState.dirty) { + setFormState({ + dirty: false, + expression: element.expression, + }); + } + }, [element, setFormState, formState]); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/expression_input/index.js b/x-pack/plugins/canvas/public/components/expression_input/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/index.js rename to x-pack/plugins/canvas/public/components/expression_input/index.ts diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index 6905b3ed23d3f..7151e72a44780 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -21,7 +21,6 @@ import { import { WorkpadManager } from '../workpad_manager'; import { RouterContext } from '../router'; import { PageManager } from '../page_manager'; -// @ts-expect-error untyped local import { Expression } from '../expression'; import { Tray } from './tray'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts index 443eb06846d2e..a2e5353a01ab2 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts @@ -7,7 +7,7 @@ jest.mock('../../../../common/lib/fetch'); import { getPdfUrl, createPdf, LayoutType } from './utils'; -import { workpads } from '../../../../__tests__/fixtures/workpads'; +import { workpads } from '../../../../__fixtures__/workpads'; import { fetch } from '../../../../common/lib/fetch'; import { IBasePath } from 'kibana/public'; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot index 1f7105b80de4c..d267ba07078fe 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot @@ -178,11 +178,7 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = ` > - } + title="; Sorted in ascending order" > Template name diff --git a/x-pack/plugins/canvas/public/functions/pie.test.js b/x-pack/plugins/canvas/public/functions/pie.test.js index 99f61cfb5d922..1ba82a3573d75 100644 --- a/x-pack/plugins/canvas/public/functions/pie.test.js +++ b/x-pack/plugins/canvas/public/functions/pie.test.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../__tests__/helpers/function_wrapper'; -import { testPie } from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_pointseries'; +import { functionWrapper } from '../../test_helpers/function_wrapper'; +import { testPie } from '../../canvas_plugin_src/functions/common/__fixtures__/test_pointseries'; import { fontStyle, grayscalePalette, seriesStyle, -} from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles'; +} from '../../canvas_plugin_src/functions/common/__fixtures__/test_styles'; import { pieFunctionFactory } from './pie'; describe('pie', () => { diff --git a/x-pack/plugins/canvas/public/functions/plot.test.js b/x-pack/plugins/canvas/public/functions/plot.test.js index 426e9a23efe5d..55414878f504f 100644 --- a/x-pack/plugins/canvas/public/functions/plot.test.js +++ b/x-pack/plugins/canvas/public/functions/plot.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionWrapper } from '../../__tests__/helpers/function_wrapper'; -import { testPlot } from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_pointseries'; +import { functionWrapper } from '../../test_helpers/function_wrapper'; +import { testPlot } from '../../canvas_plugin_src/functions/common/__fixtures__/test_pointseries'; import { fontStyle, grayscalePalette, @@ -13,7 +13,7 @@ import { xAxisConfig, seriesStyle, defaultStyle, -} from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles'; +} from '../../canvas_plugin_src/functions/common/__fixtures__/test_styles'; import { plotFunctionFactory } from './plot'; describe('plot', () => { diff --git a/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.test.js b/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.test.js index c0ab3bb316b73..ad988fd533d01 100644 --- a/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.test.js +++ b/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.test.js @@ -8,7 +8,7 @@ import { xAxisConfig, yAxisConfig, hideAxis, -} from '../../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles'; +} from '../../../canvas_plugin_src/functions/common/__fixtures__/test_styles'; import { getFlotAxisConfig } from './get_flot_axis_config'; describe('getFlotAxisConfig', () => { diff --git a/x-pack/plugins/canvas/public/functions/plot/get_font_spec.test.js b/x-pack/plugins/canvas/public/functions/plot/get_font_spec.test.js index dfaf510a94718..a60e0c038a0c1 100644 --- a/x-pack/plugins/canvas/public/functions/plot/get_font_spec.test.js +++ b/x-pack/plugins/canvas/public/functions/plot/get_font_spec.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fontStyle } from '../../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles'; +import { fontStyle } from '../../../canvas_plugin_src/functions/common/__fixtures__/test_styles'; import { defaultSpec, getFontSpec } from './get_font_spec'; describe('getFontSpec', () => { diff --git a/x-pack/plugins/canvas/public/lib/clipboard.test.ts b/x-pack/plugins/canvas/public/lib/clipboard.test.ts index 53f92e2184edc..4df6062f9329a 100644 --- a/x-pack/plugins/canvas/public/lib/clipboard.test.ts +++ b/x-pack/plugins/canvas/public/lib/clipboard.test.ts @@ -8,7 +8,7 @@ jest.mock('../../../../../src/plugins/kibana_utils/public'); import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { setClipboardData, getClipboardData } from './clipboard'; import { LOCALSTORAGE_CLIPBOARD } from '../../common/lib/constants'; -import { elements } from '../../__tests__/fixtures/workpads'; +import { elements } from '../../__fixtures__/workpads'; const set = jest.fn(); const get = jest.fn(); diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 9bc4bd5e78fd0..4c9dbd92d3f21 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -26,6 +26,9 @@ export const createHandlers = (): RendererHandlers => ({ getRenderMode() { return 'display'; }, + isSyncColorsEnabled() { + return false; + }, onComplete(fn: () => void) { this.done = fn; }, diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index d18f1b8d24489..3c6c0d68da3db 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -26,9 +26,6 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; -// @ts-expect-error untyped local -import { argTypeSpecs } from './expression_types/arg_types'; -import { transitions } from './transitions'; import { getPluginApi, CanvasApi } from './plugin_api'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; export { CoreStart, CoreSetup }; @@ -123,8 +120,15 @@ export class CanvasPlugin plugins.home.featureCatalogue.register(featureCatalogueEntry); } - canvasApi.addArgumentUIs(argTypeSpecs); - canvasApi.addTransitions(transitions); + canvasApi.addArgumentUIs(async () => { + // @ts-expect-error + const { argTypeSpecs } = await import('./expression_types/arg_types'); + return argTypeSpecs; + }); + canvasApi.addTransitions(async () => { + const { transitions } = await import('./transitions'); + return transitions; + }); return { ...canvasApi, diff --git a/x-pack/plugins/canvas/public/plugin_api.ts b/x-pack/plugins/canvas/public/plugin_api.ts index 62e82df4b0d04..be267bb91a909 100644 --- a/x-pack/plugins/canvas/public/plugin_api.ts +++ b/x-pack/plugins/canvas/public/plugin_api.ts @@ -12,24 +12,26 @@ import { import { ElementFactory } from '../types'; import { ExpressionsSetup } from '../../../../src/plugins/expressions/public'; -type AddToRegistry = (add: T[]) => void; +type SpecPromiseFn = () => Promise; +type AddToRegistry = (add: T[] | SpecPromiseFn) => void; +type AddSpecsToRegistry = (add: T[]) => void; export interface CanvasApi { addArgumentUIs: AddToRegistry; addDatasourceUIs: AddToRegistry; addElements: AddToRegistry; - addFunctions: AddToRegistry<() => AnyExpressionFunctionDefinition>; + addFunctions: AddSpecsToRegistry<() => AnyExpressionFunctionDefinition>; addModelUIs: AddToRegistry; - addRenderers: AddToRegistry; + addRenderers: AddSpecsToRegistry; addTagUIs: AddToRegistry; addTransformUIs: AddToRegistry; addTransitions: AddToRegistry; - addTypes: AddToRegistry<() => AnyExpressionTypeDefinition>; + addTypes: AddSpecsToRegistry<() => AnyExpressionTypeDefinition>; addViewUIs: AddToRegistry; } -export interface SetupRegistries { - elements: ElementFactory[]; +export interface SetupRegistries extends Record { + elements: Array>; transformUIs: any[]; datasourceUIs: any[]; modelUIs: any[]; @@ -53,6 +55,16 @@ export function getPluginApi( transitions: [], }; + const addToRegistry = (registry: Array>) => { + return (entries: T[] | SpecPromiseFn) => { + if (Array.isArray(entries)) { + registry.push(...entries); + } else { + registry.push(entries); + } + }; + }; + const api: CanvasApi = { // Functions, types and renderers are registered directly to expression plugin addFunctions: (fns) => { @@ -75,14 +87,14 @@ export function getPluginApi( }, // All these others are local to canvas, and they will only register on start - addElements: (elements) => registries.elements.push(...elements), - addTransformUIs: (transforms) => registries.transformUIs.push(...transforms), - addDatasourceUIs: (datasources) => registries.datasourceUIs.push(...datasources), - addModelUIs: (models) => registries.modelUIs.push(...models), - addViewUIs: (views) => registries.viewUIs.push(...views), - addArgumentUIs: (args) => registries.argumentUIs.push(...args), - addTagUIs: (tags) => registries.tagUIs.push(...tags), - addTransitions: (transitions) => registries.transitions.push(...transitions), + addElements: addToRegistry(registries.elements), + addTransformUIs: addToRegistry(registries.transformUIs), + addDatasourceUIs: addToRegistry(registries.datasourceUIs), + addModelUIs: addToRegistry(registries.modelUIs), + addViewUIs: addToRegistry(registries.viewUIs), + addArgumentUIs: addToRegistry(registries.argumentUIs), + addTagUIs: addToRegistry(registries.tagUIs), + addTransitions: addToRegistry(registries.transitions), }; return { api, registries }; diff --git a/x-pack/plugins/canvas/public/registries.ts b/x-pack/plugins/canvas/public/registries.ts index b2881fc0b7799..5f87beb207b8c 100644 --- a/x-pack/plugins/canvas/public/registries.ts +++ b/x-pack/plugins/canvas/public/registries.ts @@ -40,8 +40,24 @@ export function initRegistries() { }); } -export function populateRegistries(setupRegistries: SetupRegistries) { - register(registries, setupRegistries); +export async function populateRegistries(setupRegistries: SetupRegistries) { + // Our setup registries could contain definitions or a function that would + // return a promise of definitions. + // We need to call all the fns and then wait for all of the promises to be resolved + const resolvedRegistries: Record = {}; + const promises = Object.entries(setupRegistries).map(async ([key, specs]) => { + const resolved = await ( + await Promise.all(specs.map((fn) => (typeof fn === 'function' ? fn() : fn))) + ).flat(); + + resolvedRegistries[key] = resolved; + }); + + // Now, wait for all of the promise registry promises to resolve and our resolved registry will be ready + // and we can proceeed + await Promise.all(promises); + + register(registries, resolvedRegistries); } export function destroyRegistries() { diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/in_flight.test.ts b/x-pack/plugins/canvas/public/state/middleware/in_flight.test.ts similarity index 93% rename from x-pack/plugins/canvas/public/state/middleware/__tests__/in_flight.test.ts rename to x-pack/plugins/canvas/public/state/middleware/in_flight.test.ts index b78c3ffcfcb1d..6a47289b8b826 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/in_flight.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/in_flight.test.ts @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - inFlightActive, - inFlightComplete, - setLoading, - setValue, -} from '../../actions/resolved_args'; -import { inFlightMiddlewareFactory } from '../in_flight'; +import { inFlightActive, inFlightComplete, setLoading, setValue } from '../actions/resolved_args'; +import { inFlightMiddlewareFactory } from './in_flight'; const next = jest.fn(); const dispatch = jest.fn(); diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.test.ts similarity index 91% rename from x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts rename to x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.test.ts index bb7b26919ef20..6bbb2dbf9d8ba 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.test.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../lib/app_state'); -jest.mock('../../../lib/router_provider'); +jest.mock('../../lib/app_state'); +jest.mock('../../lib/router_provider'); -import { workpadAutoplay } from '../workpad_autoplay'; -import { setAutoplayInterval } from '../../../lib/app_state'; -import { createTimeInterval } from '../../../lib/time_interval'; +import { workpadAutoplay } from './workpad_autoplay'; +import { setAutoplayInterval } from '../../lib/app_state'; +import { createTimeInterval } from '../../lib/time_interval'; // @ts-expect-error untyped local -import { routerProvider } from '../../../lib/router_provider'; +import { routerProvider } from '../../lib/router_provider'; const next = jest.fn(); const dispatch = jest.fn(); diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.test.ts similarity index 92% rename from x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts rename to x-pack/plugins/canvas/public/state/middleware/workpad_refresh.test.ts index e451a39df06db..71d3ad3a92f09 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.test.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../lib/app_state'); +jest.mock('../../lib/app_state'); -import { workpadRefresh } from '../workpad_refresh'; -import { inFlightComplete } from '../../actions/resolved_args'; -import { setRefreshInterval } from '../../actions/workpad'; -import { setRefreshInterval as setAppStateRefreshInterval } from '../../../lib/app_state'; +import { workpadRefresh } from './workpad_refresh'; +import { inFlightComplete } from '../actions/resolved_args'; +import { setRefreshInterval } from '../actions/workpad'; +import { setRefreshInterval as setAppStateRefreshInterval } from '../../lib/app_state'; -import { createTimeInterval } from '../../../lib/time_interval'; +import { createTimeInterval } from '../../lib/time_interval'; const next = jest.fn(); const dispatch = jest.fn(); diff --git a/x-pack/plugins/canvas/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts index a084e8fe3349e..9cb015eed4c5b 100644 --- a/x-pack/plugins/canvas/server/collectors/collector.ts +++ b/x-pack/plugins/canvas/server/collectors/collector.ts @@ -37,9 +37,9 @@ export function registerCanvasUsageCollector( const canvasCollector = usageCollection.makeUsageCollector({ type: 'canvas', isReady: () => true, - fetch: async ({ callCluster }: CollectorFetchContext) => { + fetch: async ({ esClient }: CollectorFetchContext) => { const collectorResults = await Promise.all( - collectors.map((collector) => collector(kibanaIndex, callCluster)) + collectors.map((collector) => collector(kibanaIndex, esClient)) ); return collectorResults.reduce((reduction, usage) => { diff --git a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts index d3ed1e17785ee..8992f78332518 100644 --- a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchParams } from 'elasticsearch'; +import { SearchResponse } from 'elasticsearch'; import { get } from 'lodash'; import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; import { collectFns } from './collector_helpers'; @@ -114,9 +114,9 @@ export function summarizeCustomElements( const customElementCollector: TelemetryCollector = async function customElementCollector( kibanaIndex, - callCluster + esClient ) { - const customElementParams: SearchParams = { + const customElementParams = { size: 10000, index: kibanaIndex, ignoreUnavailable: true, @@ -124,7 +124,9 @@ const customElementCollector: TelemetryCollector = async function customElementC body: { query: { bool: { filter: { term: { type: CUSTOM_ELEMENT_TYPE } } } } }, }; - const esResponse = await callCluster('search', customElementParams); + const { body: esResponse } = await esClient.search>( + customElementParams + ); if (get(esResponse, 'hits.hits.length') > 0) { const customElements = esResponse.hits.hits.map((hit) => hit._source[CUSTOM_ELEMENT_TYPE]); diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts index 32665cc42dc4e..919f1537e81b3 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts @@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash'; import { summarizeWorkpads } from './workpad_collector'; -import { workpads } from '../../__tests__/fixtures/workpads'; +import { workpads } from '../../__fixtures__/workpads'; describe('usage collector handle es response data', () => { it('should summarize workpads, pages, and elements', () => { diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts index 0479411528802..5d633a0d4dd1d 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchParams } from 'elasticsearch'; +import { SearchResponse } from 'elasticsearch'; import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash'; import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; import { CANVAS_TYPE } from '../../common/lib/constants'; @@ -229,9 +229,10 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr variables: variableInfo, }; } +type ESResponse = SearchResponse; -const workpadCollector: TelemetryCollector = async function (kibanaIndex, callCluster) { - const searchParams: SearchParams = { +const workpadCollector: TelemetryCollector = async function (kibanaIndex, esClient) { + const searchParams = { size: 10000, // elasticsearch index.max_result_window default value index: kibanaIndex, ignoreUnavailable: true, @@ -239,7 +240,7 @@ const workpadCollector: TelemetryCollector = async function (kibanaIndex, callCl body: { query: { bool: { filter: { term: { type: CANVAS_TYPE } } } } }, }; - const esResponse = await callCluster('search', searchParams); + const { body: esResponse } = await esClient.search(searchParams); if (get(esResponse, 'hits.hits.length') > 0) { const workpads = esResponse.hits.hits.map((hit) => hit._source[CANVAS_TYPE]); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts index a51cbefd4031e..0e0db5a1093df 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -8,7 +8,7 @@ import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeGetWorkpadRoute } from './get'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; -import { workpadWithGroupAsElement } from '../../../__tests__/fixtures/workpads'; +import { workpadWithGroupAsElement } from '../../../__fixtures__/workpads'; import { CanvasWorkpad } from '../../../types'; import { getMockedRouterDeps } from '../test_helpers'; diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts index 0d97145c90298..e2e84a9a88cb6 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -9,7 +9,7 @@ import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; -import { workpads } from '../../../__tests__/fixtures/workpads'; +import { workpads } from '../../../__fixtures__/workpads'; import { okResponse } from '../ok_response'; import { getMockedRouterDeps } from '../test_helpers'; diff --git a/x-pack/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/api/__snapshots__/shareable.test.tsx.snap similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap rename to x-pack/plugins/canvas/shareable_runtime/api/__snapshots__/shareable.test.tsx.snap diff --git a/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx b/x-pack/plugins/canvas/shareable_runtime/api/shareable.test.tsx similarity index 96% rename from x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx rename to x-pack/plugins/canvas/shareable_runtime/api/shareable.test.tsx index 0851ae8d04eb0..9efb8db805268 100644 --- a/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/api/shareable.test.tsx @@ -6,11 +6,11 @@ import { mount } from 'enzyme'; import React from 'react'; -import { sharedWorkpads, tick } from '../../test'; -import { share } from '../shareable'; +import { sharedWorkpads, tick } from '../test'; +import { share } from './shareable'; // Mock the renderers within this test. -jest.mock('../../supported_renderers'); +jest.mock('../supported_renderers'); describe('Canvas Shareable Workpad API', () => { // Mock the AJAX load of the workpad. diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/__snapshots__/app.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__tests__/__snapshots__/app.test.tsx.snap rename to x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx similarity index 95% rename from x-pack/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index 755f6907a4d5b..13e7738b9fd6b 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -13,8 +13,8 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; // import { act } from 'react-dom/test-utils'; -import { App } from '../app'; -import { sharedWorkpads, WorkpadNames, tick } from '../../test'; +import { App } from './app'; +import { sharedWorkpads, WorkpadNames, tick } from '../test'; import { getScrubber as scrubber, getScrubberSlideContainer as scrubberContainer, @@ -27,11 +27,11 @@ import { getFooter as footer, getPageControlsPrevious as previous, getPageControlsNext as next, -} from '../../test/selectors'; -import { openSettings, selectMenuItem } from '../../test/interactions'; +} from '../test/selectors'; +import { openSettings, selectMenuItem } from '../test/interactions'; // Mock the renderers -jest.mock('../../supported_renderers'); +jest.mock('../supported_renderers'); // Mock the EuiPortal - `insertAdjacentElement is not supported in // `jsdom` 12. We're just going to render a `div` with the children diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/canvas.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/canvas.test.tsx similarity index 84% rename from x-pack/plugins/canvas/shareable_runtime/components/__tests__/canvas.test.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/canvas.test.tsx index deb524ed56bc5..b1e6284f6e6a9 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/canvas.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/canvas.test.tsx @@ -6,11 +6,11 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { JestContext } from '../../test/context_jest'; -import { getScrubber as scrubber, getPageControlsCenter as center } from '../../test/selectors'; -import { Canvas } from '../canvas'; +import { JestContext } from '../test/context_jest'; +import { getScrubber as scrubber, getPageControlsCenter as center } from '../test/selectors'; +import { Canvas } from './canvas'; -jest.mock('../../supported_renderers'); +jest.mock('../supported_renderers'); describe('', () => { test('null workpad renders nothing', () => { diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx index ba938b57f1ead..371f646bf8619 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx @@ -10,7 +10,7 @@ import { storiesOf } from '@storybook/react'; import { CanvasRenderedPage } from '../../../types'; import { ExampleContext } from '../../../test/context_example'; import { Scrubber, ScrubberComponent } from '../scrubber'; -import { workpads } from '../../../../__tests__/fixtures/workpads'; +import { workpads } from '../../../../__fixtures__/workpads'; storiesOf('shareables/Footer/Scrubber', module) .add('contextual: hello', () => ( diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/page.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/page.test.tsx similarity index 93% rename from x-pack/plugins/canvas/shareable_runtime/components/__tests__/page.test.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/page.test.tsx index 7e3f5b1cd3555..8e780e24eecfb 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/page.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/page.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { Page } from '../page'; +import { Page } from './page'; describe('', () => { test('null workpad renders nothing', () => { diff --git a/x-pack/plugins/canvas/__tests__/helpers/function_wrapper.js b/x-pack/plugins/canvas/test_helpers/function_wrapper.js similarity index 100% rename from x-pack/plugins/canvas/__tests__/helpers/function_wrapper.js rename to x-pack/plugins/canvas/test_helpers/function_wrapper.js diff --git a/x-pack/plugins/canvas/types/telemetry.ts b/x-pack/plugins/canvas/types/telemetry.ts index 3b635b2e57926..e323372e87993 100644 --- a/x-pack/plugins/canvas/types/telemetry.ts +++ b/x-pack/plugins/canvas/types/telemetry.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; /** Function for collecting information about canvas usage @@ -13,7 +13,7 @@ export type TelemetryCollector = ( /** The server instance */ kibanaIndex: string, /** Function for calling elasticsearch */ - callCluster: LegacyAPICaller + esClient: ElasticsearchClient ) => Record; export interface TelemetryCustomElementDocument { diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index 84f0e1fea6edf..b82c6de8fc363 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -41,6 +41,7 @@ export const CaseConfigureResponseRt = rt.intersection([ ConnectorMappingsRt, rt.type({ version: rt.string, + error: rt.union([rt.string, rt.null]), }), ]); diff --git a/x-pack/plugins/case/server/client/alerts/update_status.test.ts b/x-pack/plugins/case/server/client/alerts/update_status.test.ts new file mode 100644 index 0000000000000..834a72b849f65 --- /dev/null +++ b/x-pack/plugins/case/server/client/alerts/update_status.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CaseStatuses } from '../../../common/api'; +import { createMockSavedObjectsRepository } from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +describe('updateAlertsStatus', () => { + describe('happy path', () => { + test('it update the status of the alert correctly', async () => { + const savedObjectsClient = createMockSavedObjectsRepository(); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + await caseClient.client.updateAlertsStatus({ + ids: ['alert-id-1'], + status: CaseStatuses.closed, + }); + + expect(caseClient.services.alertsService.updateAlertsStatus).toHaveBeenCalledWith({ + ids: ['alert-id-1'], + index: '.siem-signals', + request: {}, + status: CaseStatuses.closed, + }); + }); + + describe('unhappy path', () => { + test('it throws when missing securitySolutionClient', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository(); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ + savedObjectsClient, + omitFromContext: ['securitySolution'], + }); + caseClient.client + .updateAlertsStatus({ + ids: ['alert-id-1'], + status: CaseStatuses.closed, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(404); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 90116e3728883..7c2091fe5e220 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -43,7 +43,7 @@ describe('create', () => { caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.create({ theCase: postCase }); expect(res).toEqual({ @@ -120,7 +120,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.create({ theCase: postCase }); expect(res).toEqual({ @@ -165,7 +165,10 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ + savedObjectsClient, + badAuth: true, + }); const res = await caseClient.client.create({ theCase: postCase }); expect(res).toEqual({ @@ -213,7 +216,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client // @ts-expect-error .create({ theCase: postCase }) @@ -240,7 +243,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client // @ts-expect-error .create({ theCase: postCase }) @@ -267,7 +270,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client // @ts-expect-error .create({ theCase: postCase }) @@ -289,7 +292,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client // @ts-expect-error .create({ theCase: postCase }) @@ -317,7 +320,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client // @ts-expect-error .create({ theCase: postCase }) @@ -349,7 +352,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client.create({ theCase: postCase }).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); @@ -375,7 +378,7 @@ describe('create', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client.create({ theCase: postCase }).catch((e) => { expect(e).not.toBeNull(); diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 1f9e8cc788404..a3ddb5f61a5ce 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -9,6 +9,7 @@ import { createMockSavedObjectsRepository, mockCaseNoConnectorId, mockCases, + mockCaseComments, } from '../../routes/api/__fixtures__'; import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; @@ -37,7 +38,7 @@ describe('update', () => { caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.update({ caseClient: caseClient.client, cases: patchCases, @@ -120,7 +121,7 @@ describe('update', () => { ], }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.update({ caseClient: caseClient.client, cases: patchCases, @@ -156,6 +157,61 @@ describe('update', () => { ]); }); + test('it change the status of case to in-progress correctly', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-4', + status: CaseStatuses['in-progress'], + version: 'WzUsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + const res = await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); + + expect(res).toEqual([ + { + closed_at: null, + closed_by: null, + comments: [], + connector: { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: { + issueType: 'Task', + parent: null, + priority: 'High', + }, + }, + created_at: '2019-11-25T22:32:17.947Z', + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, + description: 'Oh no, a bad meanie going LOLBins all over the place!', + id: 'mock-id-4', + external_service: null, + status: CaseStatuses['in-progress'], + tags: ['LOLBins'], + title: 'Another bad one', + totalComment: 0, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, + }, + ]); + }); + test('it updates a case without a connector.id', async () => { const patchCases = { cases: [ @@ -171,7 +227,7 @@ describe('update', () => { caseSavedObject: [mockCaseNoConnectorId], }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.update({ caseClient: caseClient.client, cases: patchCases, @@ -227,7 +283,7 @@ describe('update', () => { caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.update({ caseClient: caseClient.client, cases: patchCases, @@ -270,6 +326,204 @@ describe('update', () => { }, ]); }); + + test('it updates alert status when the status is updated and syncAlerts=true', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: CaseStatuses.closed, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: [{ ...mockCaseComments[3] }], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); + + expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({ + ids: ['test-id'], + status: 'closed', + }); + }); + + test('it does NOT updates alert status when the status is updated and syncAlerts=false', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: CaseStatuses.closed, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [ + { + ...mockCases[0], + attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, + }, + ], + caseCommentSavedObject: [{ ...mockCaseComments[3] }], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); + + expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled(); + }); + + test('it updates alert status when syncAlerts is turned on', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + settings: { syncAlerts: true }, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [ + { + ...mockCases[0], + attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, + }, + ], + caseCommentSavedObject: [{ ...mockCaseComments[3] }], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); + + expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({ + ids: ['test-id'], + status: 'open', + }); + }); + + test('it does NOT updates alert status when syncAlerts is turned off', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + settings: { syncAlerts: false }, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: [{ ...mockCaseComments[3] }], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); + + expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled(); + }); + + test('it updates alert status for multiple cases', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + settings: { syncAlerts: true }, + version: 'WzAsMV0=', + }, + { + id: 'mock-id-2', + status: CaseStatuses.closed, + version: 'WzQsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [ + { + ...mockCases[0], + attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, + }, + { + ...mockCases[1], + }, + ], + caseCommentSavedObject: [{ ...mockCaseComments[3] }, { ...mockCaseComments[4] }], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); + + expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(1, { + ids: ['test-id', 'test-id-2'], + status: 'open', + }); + + expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(2, { + ids: ['test-id', 'test-id-2'], + status: 'closed', + }); + }); + + test('it does NOT call updateAlertsStatus when there is no comments of type alerts', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: CaseStatuses.closed, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); + + expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled(); + }); }); describe('unhappy path', () => { @@ -293,7 +547,7 @@ describe('update', () => { caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client // @ts-expect-error .update({ cases: patchCases }) @@ -324,7 +578,7 @@ describe('update', () => { caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client // @ts-expect-error .update({ cases: patchCases }) @@ -351,7 +605,7 @@ describe('update', () => { caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); @@ -381,7 +635,7 @@ describe('update', () => { caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); @@ -408,7 +662,7 @@ describe('update', () => { caseSavedObject: mockCases, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts index e2b6cb8337251..3dc3921c23cf4 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -110,7 +110,8 @@ export const update = ({ }; } else if ( updateCaseAttributes.status && - updateCaseAttributes.status === CaseStatuses.open + (updateCaseAttributes.status === CaseStatuses.open || + updateCaseAttributes.status === CaseStatuses['in-progress']) ) { closedInfo = { closed_at: null, @@ -182,11 +183,14 @@ export const update = ({ // The filter guarantees that the comments will be of type alert })) as SavedObjectsFindResponse<{ alertId: string }>; - caseClient.updateAlertsStatus({ - ids: caseComments.saved_objects.map(({ attributes: { alertId } }) => alertId), - // Either there is a status update or the syncAlerts got turned on. - status: theCase.status ?? currentCase?.attributes.status ?? CaseStatuses.open, - }); + const commentIds = caseComments.saved_objects.map(({ attributes: { alertId } }) => alertId); + if (commentIds.length > 0) { + caseClient.updateAlertsStatus({ + ids: commentIds, + // Either there is a status update or the syncAlerts got turned on. + status: theCase.status ?? currentCase?.attributes.status ?? CaseStatuses.open, + }); + } } const returnUpdatedCase = myCases.saved_objects diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index 40b87f6ad17f0..85967d4d79cc4 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -29,7 +29,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.addComment({ caseClient: caseClient.client, caseId: 'mock-id-1', @@ -65,7 +65,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.addComment({ caseClient: caseClient.client, caseId: 'mock-id-1', @@ -103,7 +103,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.addComment({ caseClient: caseClient.client, caseId: 'mock-id-1', @@ -127,7 +127,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); await caseClient.client.addComment({ caseClient: caseClient.client, caseId: 'mock-id-1', @@ -175,7 +175,10 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ + savedObjectsClient, + badAuth: true, + }); const res = await caseClient.client.addComment({ caseClient: caseClient.client, caseId: 'mock-id-1', @@ -203,6 +206,66 @@ describe('addComment', () => { version: 'WzksMV0=', }); }); + + test('it update the status of the alert if the case is synced with alerts', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ + savedObjectsClient, + badAuth: true, + }); + + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.addComment({ + caseClient: caseClient.client, + caseId: 'mock-id-1', + comment: { + type: CommentType.alert, + alertId: 'test-alert', + index: 'test-index', + }, + }); + + expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({ + ids: ['test-alert'], + status: 'open', + }); + }); + + test('it should NOT update the status of the alert if the case is NOT synced with alerts', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [ + { + ...mockCases[0], + attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, + }, + ], + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ + savedObjectsClient, + badAuth: true, + }); + + caseClient.client.updateAlertsStatus = jest.fn(); + + await caseClient.client.addComment({ + caseClient: caseClient.client, + caseId: 'mock-id-1', + comment: { + type: CommentType.alert, + alertId: 'test-alert', + index: 'test-index', + }, + }); + + expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled(); + }); }); describe('unhappy path', () => { @@ -213,7 +276,7 @@ describe('addComment', () => { caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client .addComment({ caseId: 'mock-id-1', @@ -235,7 +298,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const allRequestAttributes = { type: CommentType.user, comment: 'a comment', @@ -267,7 +330,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['alertId', 'index'].forEach((attribute) => { caseClient.client @@ -296,7 +359,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const allRequestAttributes = { type: CommentType.alert, index: 'test-index', @@ -329,7 +392,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['comment'].forEach((attribute) => { caseClient.client @@ -358,7 +421,7 @@ describe('addComment', () => { caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client .addComment({ caseClient: caseClient.client, @@ -382,7 +445,7 @@ describe('addComment', () => { caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); caseClient.client .addComment({ caseClient: caseClient.client, @@ -398,5 +461,31 @@ describe('addComment', () => { expect(e.output.statusCode).toBe(400); }); }); + + test('it throws when the case is closed and the comment is of type alert', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); + caseClient.client + .addComment({ + caseClient: caseClient.client, + caseId: 'mock-id-4', + comment: { + type: CommentType.alert, + alertId: 'test-alert', + index: 'test-index', + }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); }); }); diff --git a/x-pack/plugins/case/server/client/configure/get_fields.test.ts b/x-pack/plugins/case/server/client/configure/get_fields.test.ts index b465d916b2292..9e39e26440b6c 100644 --- a/x-pack/plugins/case/server/client/configure/get_fields.test.ts +++ b/x-pack/plugins/case/server/client/configure/get_fields.test.ts @@ -22,7 +22,7 @@ describe('get_fields', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseMappingsSavedObject: mockCaseMappings, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.getFields({ actionsClient: actionsMock, connectorType: ConnectorTypes.jira, @@ -43,7 +43,7 @@ describe('get_fields', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseMappingsSavedObject: mockCaseMappings, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); await caseClient.client .getFields({ actionsClient: { ...actionsMock, execute: jest.fn().mockReturnValue(actionsErrResponse) }, diff --git a/x-pack/plugins/case/server/client/configure/get_mappings.test.ts b/x-pack/plugins/case/server/client/configure/get_mappings.test.ts index e68db5cde940b..06f24190e2563 100644 --- a/x-pack/plugins/case/server/client/configure/get_mappings.test.ts +++ b/x-pack/plugins/case/server/client/configure/get_mappings.test.ts @@ -27,7 +27,7 @@ describe('get_mappings', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseMappingsSavedObject: mockCaseMappings, }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.getMappings({ actionsClient: actionsMock, caseClient: caseClient.client, @@ -41,7 +41,7 @@ describe('get_mappings', () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseMappingsSavedObject: [], }); - const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); const res = await caseClient.client.getMappings({ actionsClient: actionsMock, caseClient: caseClient.client, diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 54af9bee2b316..78cb7f71cef4c 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; -import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { actionsClientMock } from '../../../actions/server/mocks'; import { - AlertService, + AlertServiceContract, CaseConfigureService, CaseService, CaseUserActionServiceSetup, @@ -29,17 +30,24 @@ export const createCaseClientMock = (): CaseClientMock => ({ updateAlertsStatus: jest.fn(), }); -export const createCaseClientWithMockSavedObjectsClient = async ( - savedObjectsClient: any, - badAuth: boolean = false -): Promise<{ +export const createCaseClientWithMockSavedObjectsClient = async ({ + savedObjectsClient, + badAuth = false, + omitFromContext = [], +}: { + savedObjectsClient: any; + badAuth?: boolean; + omitFromContext?: string[]; +}): Promise<{ client: CaseClient; - services: { userActionService: jest.Mocked }; + services: { + userActionService: jest.Mocked; + alertsService: jest.Mocked; + }; }> => { const actionsMock = actionsClientMock.create(); actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); const log = loggingSystemMock.create().get('case'); - const esClientMock = elasticsearchServiceMock.createClusterClient(); const request = {} as KibanaRequest; const caseServicePlugin = new CaseService(log); @@ -56,10 +64,10 @@ export const createCaseClientWithMockSavedObjectsClient = async ( postUserActions: jest.fn(), getUserActions: jest.fn(), }; - const alertsService = new AlertService(); - alertsService.initialize(esClientMock); - const context = ({ + const alertsService = { initialize: jest.fn(), updateAlertsStatus: jest.fn() }; + + const context = { core: { savedObjects: { client: savedObjectsClient, @@ -74,7 +82,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ( getSignalsIndex: () => '.siem-signals', }), }, - } as unknown) as RequestHandlerContext; + }; const caseClient = createCaseClient({ savedObjectsClient, @@ -84,10 +92,10 @@ export const createCaseClientWithMockSavedObjectsClient = async ( connectorMappingsService, userActionService, alertsService, - context, + context: (omit(omitFromContext, context) as unknown) as RequestHandlerContext, }); return { client: caseClient, - services: { userActionService }, + services: { userActionService, alertsService }, }; }; 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 1335d6107744c..9010d1bcbe878 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 @@ -28,7 +28,7 @@ export const createMockSavedObjectsRepository = ({ caseCommentSavedObject?: any[]; caseConfigureSavedObject?: any[]; caseMappingsSavedObject?: any[]; -}) => { +} = {}) => { const mockSavedObjectsClientContract = ({ bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => { return { @@ -100,9 +100,12 @@ export const createMockSavedObjectsRepository = ({ } if ( - findArgs.type === CASE_CONFIGURE_SAVED_OBJECT && - caseConfigureSavedObject[0] && - caseConfigureSavedObject[0].id === 'throw-error-find' + (findArgs.type === CASE_CONFIGURE_SAVED_OBJECT && + caseConfigureSavedObject[0] && + caseConfigureSavedObject[0].id === 'throw-error-find') || + (findArgs.type === CASE_SAVED_OBJECT && + caseSavedObject[0] && + caseSavedObject[0].id === 'throw-error-find') ) { throw SavedObjectsErrorHelpers.createGenericNotFoundError('Error thrown for testing'); } 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 0d78bceeaf2fa..3d4bc8f76815b 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 @@ -348,6 +348,38 @@ export const mockCaseComments: Array> = [ updated_at: '2019-11-25T22:32:30.608Z', version: 'WzYsMV0=', }, + { + type: 'cases-comment', + id: 'mock-comment-5', + attributes: { + type: CommentType.alert, + index: 'test-index-2', + alertId: 'test-id-2', + created_at: '2019-11-25T22:32:30.608Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T22:32:30.608Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + references: [ + { + type: 'cases', + name: 'associated-cases', + id: 'mock-id-4', + }, + ], + updated_at: '2019-11-25T22:32:30.608Z', + version: 'WzYsMV0=', + }, ]; export const mockCaseConfigure: Array> = [ 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 index 87e165f8e0014..30df69323e4dd 100644 --- 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 @@ -19,6 +19,7 @@ import { initGetCaseConfigure } from './get_configure'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { mappings } from '../../../../client/configure/mock'; import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CaseClient } from '../../../../client'; describe('GET configuration', () => { let routeHandler: RequestHandler; @@ -43,6 +44,7 @@ describe('GET configuration', () => { expect(res.status).toEqual(200); expect(res.payload).toEqual({ ...mockCaseConfigure[0].attributes, + error: null, mappings: mappings[ConnectorTypes.jira], version: mockCaseConfigure[0].version, }); @@ -77,6 +79,7 @@ describe('GET configuration', () => { email: 'testemail@elastic.co', username: 'elastic', }, + error: null, mappings: mappings[ConnectorTypes.jira], updated_at: '2020-04-09T09:43:51.778Z', updated_by: { @@ -122,4 +125,40 @@ describe('GET configuration', () => { expect(res.status).toEqual(404); expect(res.payload.isBoom).toEqual(true); }); + + it('returns an error when mappings request throws', async () => { + const req = httpServerMock.createKibanaRequest({ + path: CASE_CONFIGURE_URL, + method: 'get', + }); + + const context = await createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: [], + }) + ); + const mockThrowContext = { + ...context, + case: { + ...context.case, + getCaseClient: () => + ({ + ...context?.case?.getCaseClient(), + getMappings: () => { + throw new Error(); + }, + } as CaseClient), + }, + }; + + const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({ + ...mockCaseConfigure[0].attributes, + error: 'Error connecting to My connector 3 instance', + mappings: [], + version: mockCaseConfigure[0].version, + }); + }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 615d4b0de17e8..6ee8b5d7e4fc2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -19,6 +19,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps }, async (context, request, response) => { try { + let error = null; const client = context.core.savedObjects.client; const myCaseConfigure = await caseConfigureService.find({ client }); @@ -35,12 +36,18 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps if (actionsClient == null) { throw Boom.notFound('Action client have not been found'); } - mappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: connector.id, - connectorType: connector.type, - }); + try { + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${connector.name} instance`; + } } return response.ok({ @@ -51,6 +58,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps connector: transformESConnectorToCaseConnector(connector), mappings, version: myCaseConfigure.saved_objects[0].version ?? '', + error, }) : {}, }); 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 index fd213a514f339..0a62c0ec7a0a2 100644 --- 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 @@ -18,6 +18,7 @@ import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPatchCaseConfigure } from './patch_configure'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CaseClient } from '../../../../client'; describe('PATCH configuration', () => { let routeHandler: RequestHandler; @@ -135,6 +136,52 @@ describe('PATCH configuration', () => { ); }); + it('patch configuration with error message for getMappings throw', async () => { + const req = httpServerMock.createKibanaRequest({ + path: CASE_CONFIGURE_URL, + method: 'patch', + body: { + closure_type: 'close-by-pushing', + connector: { + id: 'connector-new', + name: 'New connector', + type: '.jira', + fields: null, + }, + version: mockCaseConfigure[0].version, + }, + }); + + const context = await createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: [], + }) + ); + const mockThrowContext = { + ...context, + case: { + ...context.case, + getCaseClient: () => + ({ + ...context?.case?.getCaseClient(), + getMappings: () => { + throw new Error(); + }, + } as CaseClient), + }, + }; + + const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + mappings: [], + error: 'Error connecting to New connector instance', + }) + ); + }); it('throw error when configuration have not being created', async () => { const req = httpServerMock.createKibanaRequest({ path: CASE_CONFIGURE_URL, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 08db2b3103422..d2f3ea2bec5b9 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -33,6 +33,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout }, async (context, request, response) => { try { + let error = null; const client = context.core.savedObjects.client; const query = pipe( CasesConfigurePatchRt.decode(request.body), @@ -68,12 +69,18 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout if (actionsClient == null) { throw Boom.notFound('Action client have not been found'); } - mappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: connector.id, - connectorType: connector.type, - }); + try { + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${connector.name} instance`; + } } const patch = await caseConfigureService.patch({ client, @@ -96,6 +103,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout ), mappings, version: patch.version ?? '', + error, }), }); } catch (error) { 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 index 5a5836f595eee..19bebe0ed5c97 100644 --- 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 @@ -19,6 +19,7 @@ import { initPostCaseConfigure } from './post_configure'; import { newConfiguration } from '../../__mocks__/request_responses'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CaseClient } from '../../../../client'; describe('POST configuration', () => { let routeHandler: RequestHandler; @@ -64,6 +65,43 @@ describe('POST configuration', () => { }) ); }); + it('create configuration with error message for getMappings throw', async () => { + const req = httpServerMock.createKibanaRequest({ + path: CASE_CONFIGURE_URL, + method: 'post', + body: newConfiguration, + }); + + const context = await createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: [], + }) + ); + const mockThrowContext = { + ...context, + case: { + ...context.case, + getCaseClient: () => + ({ + ...context?.case?.getCaseClient(), + getMappings: () => { + throw new Error(); + }, + } as CaseClient), + }, + }; + + const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + mappings: [], + error: 'Error connecting to My connector 2 instance', + }) + ); + }); it('create configuration without authentication', async () => { routeHandler = await createRoute(initPostCaseConfigure, 'post', true); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index 8ae4e1211f5f1..b90bdd448d4da 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -13,6 +13,7 @@ import { CasesConfigureRequestRt, CaseConfigureResponseRt, throwErrors, + ConnectorMappingsAttributes, } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; @@ -32,6 +33,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route }, async (context, request, response) => { try { + let error = null; if (!context.case) { throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } @@ -58,12 +60,19 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route const { email, full_name, username } = await caseService.getUser({ request, response }); const creationDate = new Date().toISOString(); - const mappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: query.connector.id, - connectorType: query.connector.type, - }); + let mappings: ConnectorMappingsAttributes[] = []; + try { + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: query.connector.id, + connectorType: query.connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${query.connector.name} instance`; + } const post = await caseConfigureService.post({ client, attributes: { @@ -83,6 +92,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route connector: transformESConnectorToCaseConnector(post.attributes.connector), mappings, version: post.version ?? '', + error, }), }); } catch (error) { diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index ca6598fcb288c..1dd2c13328685 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -104,7 +104,7 @@ describe('GET case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.comments).toHaveLength(4); + expect(response.payload.comments).toHaveLength(5); }); it(`returns an error when thrown from getAllCaseComments`, async () => { diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts new file mode 100644 index 0000000000000..a5fe5bb3695a9 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCases, +} from '../../__fixtures__'; +import { initGetCasesStatusApi } from './get_status'; +import { CASE_STATUS_URL } from '../../../../../common/constants'; + +describe('GET status', () => { + let routeHandler: RequestHandler; + const findArgs = { + fields: [], + page: 1, + perPage: 1, + type: 'cases', + }; + + beforeAll(async () => { + routeHandler = await createRoute(initGetCasesStatusApi, 'get'); + }); + + it(`returns the status`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_STATUS_URL, + method: 'get', + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { + ...findArgs, + filter: 'cases.attributes.status: open', + }); + + expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { + ...findArgs, + filter: 'cases.attributes.status: in-progress', + }); + + expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { + ...findArgs, + filter: 'cases.attributes.status: closed', + }); + + expect(response.payload).toEqual({ + count_open_cases: 4, + count_in_progress_cases: 4, + count_closed_cases: 4, + }); + }); + + it(`returns an error when findCases throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_STATUS_URL, + method: 'get', + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: [{ ...mockCases[0], id: 'throw-error-find' }], + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(404); + }); +}); diff --git a/x-pack/plugins/case/server/services/alerts/index.test.ts b/x-pack/plugins/case/server/services/alerts/index.test.ts new file mode 100644 index 0000000000000..c0edf4516d3fb --- /dev/null +++ b/x-pack/plugins/case/server/services/alerts/index.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; +import { CaseStatuses } from '../../../common/api'; +import { AlertService, AlertServiceContract } from '.'; + +describe('updateAlertsStatus', () => { + const esClientMock = elasticsearchServiceMock.createClusterClient(); + + describe('happy path', () => { + let alertService: AlertServiceContract; + const args = { + ids: ['alert-id-1'], + index: '.siem-signals', + request: {} as KibanaRequest, + status: CaseStatuses.closed, + }; + + beforeEach(async () => { + alertService = new AlertService(); + jest.restoreAllMocks(); + }); + + test('it update the status of the alert correctly', async () => { + alertService.initialize(esClientMock); + await alertService.updateAlertsStatus(args); + + expect(esClientMock.asScoped().asCurrentUser.updateByQuery).toHaveBeenCalledWith({ + body: { + query: { ids: { values: args.ids } }, + script: { lang: 'painless', source: `ctx._source.signal.status = '${args.status}'` }, + }, + conflicts: 'abort', + ignore_unavailable: true, + index: args.index, + }); + }); + + describe('unhappy path', () => { + test('it throws when service is already initialized', async () => { + alertService.initialize(esClientMock); + expect(() => { + alertService.initialize(esClientMock); + }).toThrow(); + }); + + test('it throws when service is not initialized and try to update the status', async () => { + await expect(alertService.updateAlertsStatus(args)).rejects.toThrow(); + }); + }); + }); +}); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx index 451254efd9648..66f64fe95ff53 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx @@ -8,10 +8,6 @@ import React from 'react'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; -import { - TriggerContextMapping, - TriggerId, -} from '../../../../../../../src/plugins/ui_actions/public'; import { CollectConfigContainer } from './components'; import { AdvancedUiActionsStart, @@ -34,15 +30,15 @@ export interface Params { }>; } -export abstract class AbstractDashboardDrilldown - implements Drilldown> { +export abstract class AbstractDashboardDrilldown + implements Drilldown { constructor(protected readonly params: Params) {} public abstract readonly id: string; - public abstract readonly supportedTriggers: () => T[]; + public abstract readonly supportedTriggers: () => string[]; - protected abstract getURL(config: Config, context: TriggerContextMapping[T]): Promise; + protected abstract getURL(config: Config, context: Context): Promise; public readonly order = 100; @@ -51,7 +47,7 @@ export abstract class AbstractDashboardDrilldown public readonly euiIcon = 'dashboardApp'; private readonly ReactCollectConfig: React.FC< - CollectConfigProps> + CollectConfigProps > = (props) => ; public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); @@ -67,15 +63,12 @@ export abstract class AbstractDashboardDrilldown return true; }; - public readonly getHref = async ( - config: Config, - context: TriggerContextMapping[T] - ): Promise => { + public readonly getHref = async (config: Config, context: Context): Promise => { const url = await this.getURL(config, context); return url.path; }; - public readonly execute = async (config: Config, context: TriggerContextMapping[T]) => { + public readonly execute = async (config: Config, context: Context) => { const url = await this.getURL(config, context); await this.params.start().core.application.navigateToApp(url.appName, { path: url.appPath }); }; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts index d2d3c37a69287..04256362da4a8 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts @@ -5,9 +5,8 @@ */ import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; -import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/data/public'; import { DrilldownConfig } from '../../../../common'; export type Config = DrilldownConfig; -export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; +export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts index ff79cda1bb215..5c959432011f2 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts @@ -9,7 +9,6 @@ import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, } from '../../../../../../../src/plugins/embeddable/public'; -import { TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; /** * We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER. @@ -21,7 +20,7 @@ import { TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; * * @param triggers */ -export function ensureNestedTriggers(triggers: TriggerId[]): TriggerId[] { +export function ensureNestedTriggers(triggers: string[]): string[] { if ( !triggers.includes(APPLY_FILTER_TRIGGER) && (triggers.includes(VALUE_CLICK_TRIGGER) || triggers.includes(SELECT_RANGE_TRIGGER)) diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index ff54e0812975d..02b086fb301c8 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -10,10 +10,6 @@ import { } from './flyout_create_drilldown'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { - TriggerContextMapping, - TriggerId, -} from '../../../../../../../../src/plugins/ui_actions/public'; import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; import { uiActionsEnhancedPluginMock } from '../../../../../../ui_actions_enhanced/public/mocks'; import { UiActionsEnhancedActionFactory } from '../../../../../../ui_actions_enhanced/public/'; @@ -54,7 +50,7 @@ interface CompatibilityParams { isValueClickTriggerSupported?: boolean; isEmbeddableEnhanced?: boolean; rootType?: string; - actionFactoriesTriggers?: TriggerId[]; + actionFactoriesTriggers?: string[]; } describe('isCompatible', () => { @@ -79,9 +75,7 @@ describe('isCompatible', () => { let embeddable = new MockEmbeddable( { id: '', viewMode: isEdit ? ViewMode.EDIT : ViewMode.VIEW }, { - supportedTriggers: (isValueClickTriggerSupported ? ['VALUE_CLICK_TRIGGER'] : []) as Array< - keyof TriggerContextMapping - >, + supportedTriggers: isValueClickTriggerSupported ? ['VALUE_CLICK_TRIGGER'] : [], } ); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index a417deb47db53..12aa2f8250e24 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; +import { Action } from '../../../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { isEnhancedEmbeddable, @@ -26,7 +26,7 @@ export interface OpenFlyoutAddDrilldownParams { start: StartServicesGetter>; } -export class FlyoutCreateDrilldownAction implements ActionByType { +export class FlyoutCreateDrilldownAction implements Action { public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; public order = 12; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 1f0570445a8fc..10d82707d0817 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; +import { Action } from '../../../../../../../../src/plugins/ui_actions/public'; import { reactToUiComponent, toMountPoint, @@ -31,7 +31,7 @@ export interface FlyoutEditDrilldownParams { start: StartServicesGetter>; } -export class FlyoutEditDrilldownAction implements ActionByType { +export class FlyoutEditDrilldownAction implements Action { public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; public order = 10; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx index 27a8d73f32944..6f000756601ea 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx @@ -21,6 +21,7 @@ test('', () => { dynamicActions: ({ state } as unknown) as DynamicActionManager, }, } as unknown) as EnhancedEmbeddable, + trigger: {} as any, }} /> ); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx index 5a04e03e03457..f834d925a6494 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx @@ -9,8 +9,11 @@ import { EuiNotificationBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useContainerState } from '../../../../../../../../src/plugins/kibana_utils/public'; import { EnhancedEmbeddableContext } from '../../../../../../embeddable_enhanced/public'; import { txtDisplayName } from './i18n'; +import { ActionExecutionContext } from '../../../../../../../../src/plugins/ui_actions/public'; -export const MenuItem: React.FC<{ context: EnhancedEmbeddableContext }> = ({ context }) => { +export const MenuItem: React.FC<{ context: ActionExecutionContext }> = ({ + context, +}) => { const { events } = useContainerState(context.embeddable.enhancements.dynamicActions.state); const count = events.length; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts index e831f87baa11c..432ee6d3070ad 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts @@ -11,23 +11,19 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsStart, } from '../../../../../ui_actions_enhanced/public'; -import { TriggerContextMapping } from '../../../../../../../src/plugins/ui_actions/public'; import { uiActionsEnhancedPluginMock } from '../../../../../ui_actions_enhanced/public/mocks'; export class MockEmbeddable extends Embeddable { public rootType = 'dashboard'; public readonly type = 'mock'; - private readonly triggers: Array = []; - constructor( - initialInput: EmbeddableInput, - params: { supportedTriggers?: Array } - ) { + private readonly triggers: string[] = []; + constructor(initialInput: EmbeddableInput, params: { supportedTriggers?: string[] }) { super(initialInput, {}, undefined); this.triggers = params.supportedTriggers ?? []; } public render(node: HTMLElement) {} public reload() {} - public supportedTriggers(): Array { + public supportedTriggers(): string[] { return this.triggers; } public getRoot() { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index e1b6493be5200..d43a50775148d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -7,23 +7,10 @@ import { CoreSetup } from 'src/core/public'; import { SetupDependencies, StartDependencies } from '../../plugin'; import { CONTEXT_MENU_TRIGGER } from '../../../../../../src/plugins/embeddable/public'; -import { EnhancedEmbeddableContext } from '../../../../embeddable_enhanced/public'; -import { - FlyoutCreateDrilldownAction, - FlyoutEditDrilldownAction, - OPEN_FLYOUT_ADD_DRILLDOWN, - OPEN_FLYOUT_EDIT_DRILLDOWN, -} from './actions'; +import { FlyoutCreateDrilldownAction, FlyoutEditDrilldownAction } from './actions'; import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown'; import { createStartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; -declare module '../../../../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [OPEN_FLYOUT_ADD_DRILLDOWN]: EnhancedEmbeddableContext; - [OPEN_FLYOUT_EDIT_DRILLDOWN]: EnhancedEmbeddableContext; - } -} - interface BootstrapParams { enableDrilldowns: boolean; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index 5bfb175ea0d00..c33b26b42cb32 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -23,6 +23,7 @@ import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/pub import { StartDependencies } from '../../../plugin'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core'; +import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public'; describe('.isConfigValid()', () => { const drilldown = new EmbeddableToDashboardDrilldown({} as any); @@ -140,7 +141,7 @@ describe('.execute() & getHref', () => { }), }, timeFieldName, - } as unknown) as ApplyGlobalFilterActionContext; + } as unknown) as ApplyGlobalFilterActionContext & EnhancedEmbeddableContext; await drilldown.execute(completeConfig, context); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index c2bf48188c313..0bd21a82af54b 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TriggerContextMapping } from '../../../../../../../src/plugins/ui_actions/public'; import { DashboardUrlGeneratorState } from '../../../../../../../src/plugins/dashboard/public'; import { + ApplyGlobalFilterActionContext, APPLY_FILTER_TRIGGER, esFilters, Filter, @@ -25,6 +25,7 @@ import { import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; import { createExtract, createInject } from '../../../../common'; +import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public'; interface EmbeddableQueryInput extends EmbeddableInput { query?: Query; @@ -32,8 +33,7 @@ interface EmbeddableQueryInput extends EmbeddableInput { timeRange?: TimeRange; } -type Trigger = typeof APPLY_FILTER_TRIGGER; -type Context = TriggerContextMapping[Trigger]; +type Context = EnhancedEmbeddableContext & ApplyGlobalFilterActionContext; export type Params = AbstractDashboardDrilldownParams; /** @@ -43,10 +43,10 @@ export type Params = AbstractDashboardDrilldownParams; * by embeddables (but not necessarily); (2) its `getURL` method depends on * `embeddable` field being present in `context`. */ -export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { +export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { public readonly id = EMBEDDABLE_TO_DASHBOARD_DRILLDOWN; - public readonly supportedTriggers = () => [APPLY_FILTER_TRIGGER] as Trigger[]; + public readonly supportedTriggers = () => [APPLY_FILTER_TRIGGER]; protected async getURL(config: Config, context: Context): Promise { const state: DashboardUrlGeneratorState = { diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index 7b14a723d7877..669c33230a34c 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -12,7 +12,7 @@ export { EqlSearchStrategyResponse, IAsyncSearchOptions, pollSearch, - BackgroundSessionSavedObjectAttributes, - BackgroundSessionFindOptions, - BackgroundSessionStatus, + SearchSessionSavedObjectAttributes, + SearchSessionStatus, + SearchSessionRequestInfo, } from './search'; diff --git a/x-pack/plugins/data_enhanced/common/search/session/status.ts b/x-pack/plugins/data_enhanced/common/search/session/status.ts index a83dd389e4f13..e3a5bc02cdd41 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/status.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum BackgroundSessionStatus { +export enum SearchSessionStatus { IN_PROGRESS = 'in_progress', ERROR = 'error', COMPLETE = 'complete', diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts index 0b82c9160ea1a..ada7988c31f30 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface BackgroundSessionSavedObjectAttributes { +export interface SearchSessionSavedObjectAttributes { /** * User-facing session name to be displayed in session management */ @@ -13,16 +13,58 @@ export interface BackgroundSessionSavedObjectAttributes { * App that created the session. e.g 'discover' */ appId: string; + /** + * Creation time of the session + */ created: string; + /** + * Expiration time of the session. Expiration itself is managed by Elasticsearch. + */ expires: string; + /** + * status + */ status: string; + /** + * urlGeneratorId + */ urlGeneratorId: string; + /** + * The application state that was used to create the session. + * Should be used, for example, to re-load an expired search session. + */ initialState: Record; + /** + * Application state that should be used to restore the session. + * For example, relative dates are conveted to absolute ones. + */ restoreState: Record; - idMapping: Record; + /** + * Mapping of search request hashes to their corresponsing info (async search id, etc.) + */ + idMapping: Record; +} + +export interface SearchSessionRequestInfo { + /** + * ID of the async search request + */ + id: string; + /** + * Search strategy used to submit the search request + */ + strategy: string; + /** + * status + */ + status: string; + /** + * An optional error. Set if status is set to error. + */ + error?: string; } -export interface BackgroundSessionFindOptions { +export interface SearchSessionFindOptions { page?: number; perPage?: number; sortField?: string; diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index eea0101ec4ed7..3951468f6e569 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -8,7 +8,8 @@ "requiredPlugins": [ "bfetch", "data", - "features" + "features", + "taskManager" ], "optionalPlugins": ["kibanaUtils", "usageCollection"], "server": true, diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index a3b37e47287e5..c7d1c8624cb1f 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -13,7 +13,7 @@ import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; import { EnhancedSearchInterceptor } from './search/search_interceptor'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { createConnectedBackgroundSessionIndicator } from './search'; +import { createConnectedSearchSessionIndicator } from './search'; import { ConfigSchema } from '../config'; export interface DataEnhancedSetupDependencies { @@ -66,7 +66,7 @@ export class DataEnhancedPlugin core.chrome.setBreadcrumbsAppendExtension({ content: toMountPoint( React.createElement( - createConnectedBackgroundSessionIndicator({ + createConnectedSearchSessionIndicator({ sessionService: plugins.data.search.session, application: core.application, timeFilter: plugins.data.query.timefilter.timefilter, diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 20b55d9688edb..fc6c860f907f6 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -9,7 +9,7 @@ import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { AbortError } from '../../../../../src/plugins/kibana_utils/public'; -import { ISessionService, SearchTimeoutError, SessionState } from 'src/plugins/data/public'; +import { ISessionService, SearchTimeoutError, SearchSessionState } from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { bfetchPluginMock } from '../../../../../src/plugins/bfetch/public/mocks'; import { BehaviorSubject } from 'rxjs'; @@ -45,12 +45,12 @@ function mockFetchImplementation(responses: any[]) { describe('EnhancedSearchInterceptor', () => { let mockUsageCollector: any; let sessionService: jest.Mocked; - let sessionState$: BehaviorSubject; + let sessionState$: BehaviorSubject; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); - sessionState$ = new BehaviorSubject(SessionState.None); + sessionState$ = new BehaviorSubject(SearchSessionState.None); const dataPluginMockStart = dataPluginMock.createStartContract(); sessionService = { ...(dataPluginMockStart.search.session as jest.Mocked), @@ -408,7 +408,7 @@ describe('EnhancedSearchInterceptor', () => { expect(next).toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); - sessionState$.next(SessionState.BackgroundLoading); + sessionState$.next(SearchSessionState.BackgroundLoading); await timeTravel(240); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index 0e87c093d2a8d..b0f194115f0b8 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -12,7 +12,7 @@ import { SearchInterceptorDeps, UI_SETTINGS, IKibanaSearchRequest, - SessionState, + SearchSessionState, } from '../../../../../src/plugins/data/public'; import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; import { ENHANCED_ES_SEARCH_STRATEGY, IAsyncSearchOptions, pollSearch } from '../../common'; @@ -77,7 +77,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.deps.session.state$ .pipe( skip(1), // ignore any state, we are only interested in transition x -> BackgroundLoading - filter((state) => isCurrentSession() && state === SessionState.BackgroundLoading), + filter((state) => isCurrentSession() && state === SearchSessionState.BackgroundLoading), take(1) ) .subscribe(() => { diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx deleted file mode 100644 index 4a6a852be755b..0000000000000 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { BackgroundSessionIndicator } from './background_session_indicator'; -import { SessionState } from '../../../../../../../src/plugins/data/public'; - -storiesOf('components/BackgroundSessionIndicator', module).add('default', () => ( - <> -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- -)); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx deleted file mode 100644 index e08773c6a8a76..0000000000000 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, waitFor, screen, act } from '@testing-library/react'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { createConnectedBackgroundSessionIndicator } from './connected_background_session_indicator'; -import { BehaviorSubject } from 'rxjs'; -import { - ISessionService, - RefreshInterval, - SessionState, - TimefilterContract, -} from '../../../../../../../src/plugins/data/public'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; - -const coreStart = coreMock.createStart(); -const dataStart = dataPluginMock.createStartContract(); -const sessionService = dataStart.search.session as jest.Mocked; - -const refreshInterval$ = new BehaviorSubject({ value: 0, pause: true }); -const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked; -timeFilter.getRefreshIntervalUpdate$.mockImplementation(() => refreshInterval$); -timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue()); - -beforeEach(() => { - refreshInterval$.next({ value: 0, pause: true }); -}); - -test("shouldn't show indicator in case no active search session", async () => { - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ - sessionService, - application: coreStart.application, - timeFilter, - }); - const { getByTestId, container } = render(); - - // make sure `backgroundSessionIndicator` isn't appearing after some time (lazy-loading) - await expect( - waitFor(() => getByTestId('backgroundSessionIndicator'), { timeout: 100 }) - ).rejects.toThrow(); - expect(container).toMatchInlineSnapshot(`
`); -}); - -test('should show indicator in case there is an active search session', async () => { - const state$ = new BehaviorSubject(SessionState.Loading); - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ - sessionService: { ...sessionService, state$ }, - application: coreStart.application, - timeFilter, - }); - const { getByTestId } = render(); - - await waitFor(() => getByTestId('backgroundSessionIndicator')); -}); - -test('should be disabled during auto-refresh', async () => { - const state$ = new BehaviorSubject(SessionState.Loading); - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ - sessionService: { ...sessionService, state$ }, - application: coreStart.application, - timeFilter, - }); - - render(); - - await waitFor(() => screen.getByTestId('backgroundSessionIndicator')); - - expect( - screen.getByTestId('backgroundSessionIndicator').querySelector('button') - ).not.toBeDisabled(); - - act(() => { - refreshInterval$.next({ value: 0, pause: false }); - }); - - expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); -}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx new file mode 100644 index 0000000000000..2c74f9c995a5a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.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 React from 'react'; +import { render, waitFor, screen, act } from '@testing-library/react'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { createConnectedSearchSessionIndicator } from './connected_search_session_indicator'; +import { BehaviorSubject } from 'rxjs'; +import { + ISessionService, + RefreshInterval, + SearchSessionState, + TimefilterContract, +} from '../../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; + +const coreStart = coreMock.createStart(); +const dataStart = dataPluginMock.createStartContract(); +const sessionService = dataStart.search.session as jest.Mocked; + +const refreshInterval$ = new BehaviorSubject({ value: 0, pause: true }); +const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked; +timeFilter.getRefreshIntervalUpdate$.mockImplementation(() => refreshInterval$); +timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue()); + +beforeEach(() => { + refreshInterval$.next({ value: 0, pause: true }); +}); + +test("shouldn't show indicator in case no active search session", async () => { + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService, + application: coreStart.application, + timeFilter, + }); + const { getByTestId, container } = render(); + + // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) + await expect( + waitFor(() => getByTestId('searchSessionIndicator'), { timeout: 100 }) + ).rejects.toThrow(); + expect(container).toMatchInlineSnapshot(`
`); +}); + +test('should show indicator in case there is an active search session', async () => { + const state$ = new BehaviorSubject(SearchSessionState.Loading); + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + }); + const { getByTestId } = render(); + + await waitFor(() => getByTestId('searchSessionIndicator')); +}); + +test('should be disabled when permissions are off', async () => { + const state$ = new BehaviorSubject(SearchSessionState.Loading); + coreStart.application.currentAppId$ = new BehaviorSubject('discover'); + (coreStart.application.capabilities as any) = { + discover: { + storeSearchSession: false, + }, + }; + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + }); + + render(); + + await waitFor(() => screen.getByTestId('searchSessionIndicator')); + + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); +}); + +test('should be disabled during auto-refresh', async () => { + const state$ = new BehaviorSubject(SearchSessionState.Loading); + coreStart.application.currentAppId$ = new BehaviorSubject('discover'); + (coreStart.application.capabilities as any) = { + discover: { + storeSearchSession: true, + }, + }; + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + }); + + render(); + + await waitFor(() => screen.getByTestId('searchSessionIndicator')); + + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).not.toBeDisabled(); + + act(() => { + refreshInterval$.next({ value: 0, pause: false }); + }); + + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx similarity index 67% rename from x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index b80295d87d202..5c8c01064bff4 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -8,37 +8,60 @@ import React from 'react'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; -import { BackgroundSessionIndicator } from '../background_session_indicator'; +import { SearchSessionIndicator } from '../search_session_indicator'; import { ISessionService, TimefilterContract } from '../../../../../../../src/plugins/data/public/'; import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; import { ApplicationStart } from '../../../../../../../src/core/public'; -export interface BackgroundSessionIndicatorDeps { +export interface SearchSessionIndicatorDeps { sessionService: ISessionService; timeFilter: TimefilterContract; application: ApplicationStart; } -export const createConnectedBackgroundSessionIndicator = ({ +export const createConnectedSearchSessionIndicator = ({ sessionService, application, timeFilter, -}: BackgroundSessionIndicatorDeps): React.FC => { +}: SearchSessionIndicatorDeps): React.FC => { const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; const isAutoRefreshEnabled$ = timeFilter .getRefreshIntervalUpdate$() .pipe(map(isAutoRefreshEnabled), distinctUntilChanged()); + const getCapabilitiesByAppId = ( + capabilities: ApplicationStart['capabilities'], + appId?: string + ) => { + switch (appId) { + case 'dashboards': + return capabilities.dashboard; + case 'discover': + return capabilities.discover; + default: + return undefined; + } + }; + return () => { const state = useObservable(sessionService.state$.pipe(debounceTime(500))); const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); + const appId = useObservable(application.currentAppId$, undefined); + let disabled = false; let disabledReasonText: string = ''; + if (getCapabilitiesByAppId(application.capabilities, appId)?.storeSearchSession !== true) { + disabled = true; + disabledReasonText = i18n.translate('xpack.data.searchSessionIndicator.noCapability', { + defaultMessage: "You don't have permissions to send to background.", + }); + } + if (autoRefreshEnabled) { disabled = true; disabledReasonText = i18n.translate( - 'xpack.data.backgroundSessionIndicator.disabledDueToAutoRefreshMessage', + 'xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage', { defaultMessage: 'Send to background is not available when auto refresh is enabled.', } @@ -48,7 +71,7 @@ export const createConnectedBackgroundSessionIndicator = ({ if (!state) return null; return ( - { sessionService.save(); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/index.ts similarity index 65% rename from x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts rename to x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/index.ts index 223a0537129df..da6ce470e6b81 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/index.ts @@ -5,6 +5,6 @@ */ export { - BackgroundSessionIndicatorDeps, - createConnectedBackgroundSessionIndicator, -} from './connected_background_session_indicator'; + SearchSessionIndicatorDeps, + createConnectedSearchSessionIndicator, +} from './connected_search_session_indicator'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_view_state.ts similarity index 85% rename from x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts rename to x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_view_state.ts index b75c2a536f624..7b9b182453082 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_view_state.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum BackgroundSessionViewState { +export enum SearchSessionViewState { /** * Pending search request has not been sent to the background yet */ Loading = 'loading', /** - * No action was taken and the page completed loading without background session creation. + * No action was taken and the page completed loading without search session creation. */ Completed = 'completed', @@ -22,7 +22,7 @@ export enum BackgroundSessionViewState { BackgroundLoading = 'backgroundLoading', /** - * Page load completed with background session created. + * Page load completed with search session created. */ BackgroundCompleted = 'backgroundCompleted', diff --git a/x-pack/plugins/data_enhanced/public/search/ui/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/index.ts index 04201325eb5db..fce8f215a4b7f 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/index.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './connected_background_session_indicator'; +export * from './connected_search_session_indicator'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/index.tsx similarity index 57% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/index.tsx index 55c8c453dd5d2..16ee6b49a761f 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/index.tsx @@ -6,8 +6,8 @@ import { EuiDelayRender, EuiLoadingSpinner } from '@elastic/eui'; import React from 'react'; -import type { BackgroundSessionIndicatorProps } from './background_session_indicator'; -export type { BackgroundSessionIndicatorProps }; +import type { SearchSessionIndicatorProps } from './search_session_indicator'; +export type { SearchSessionIndicatorProps }; const Fallback = () => ( @@ -15,9 +15,9 @@ const Fallback = () => ( ); -const LazyBackgroundSessionIndicator = React.lazy(() => import('./background_session_indicator')); -export const BackgroundSessionIndicator = (props: BackgroundSessionIndicatorProps) => ( +const LazySearchSessionIndicator = React.lazy(() => import('./search_session_indicator')); +export const SearchSessionIndicator = (props: SearchSessionIndicatorProps) => ( }> - + ); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.scss similarity index 69% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.scss index 2d13d320ae78b..6f3ae5b5846fd 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.scss @@ -1,14 +1,14 @@ -.backgroundSessionIndicator { +.searchSessionIndicator { padding: 0 $euiSizeXS; } @include euiBreakpoint('xs', 's') { - .backgroundSessionIndicator__popoverContainer.euiFlexGroup--responsive .euiFlexItem { + .searchSessionIndicator__popoverContainer.euiFlexGroup--responsive .euiFlexItem { margin-bottom: $euiSizeXS !important; } } -.backgroundSessionIndicator__verticalDivider { +.searchSessionIndicator__verticalDivider { @include euiBreakpoint('xs', 's') { margin-left: $euiSizeXS; padding-left: $euiSizeXS; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx new file mode 100644 index 0000000000000..f3b526a8743ea --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { SearchSessionIndicator } from './search_session_indicator'; +import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; + +storiesOf('components/SearchSessionIndicator', module).add('default', () => ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +)); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx similarity index 60% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx index b7d342300f311..6cefa1237f357 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx @@ -7,9 +7,9 @@ import React, { ReactNode } from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { BackgroundSessionIndicator } from './background_session_indicator'; +import { SearchSessionIndicator } from './search_session_indicator'; import { IntlProvider } from 'react-intl'; -import { SessionState } from '../../../../../../../src/plugins/data/public'; +import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; function Container({ children }: { children?: ReactNode }) { return {children}; @@ -19,12 +19,12 @@ test('Loading state', async () => { const onCancel = jest.fn(); render( - + ); - await userEvent.click(screen.getByLabelText('Loading results')); - await userEvent.click(screen.getByText('Cancel')); + await userEvent.click(screen.getByLabelText('Loading')); + await userEvent.click(screen.getByText('Cancel session')); expect(onCancel).toBeCalled(); }); @@ -33,12 +33,12 @@ test('Completed state', async () => { const onSave = jest.fn(); render( - + ); - await userEvent.click(screen.getByLabelText('Results loaded')); - await userEvent.click(screen.getByText('Save')); + await userEvent.click(screen.getByLabelText('Loaded')); + await userEvent.click(screen.getByText('Save session')); expect(onSave).toBeCalled(); }); @@ -47,12 +47,12 @@ test('Loading in the background state', async () => { const onCancel = jest.fn(); render( - + ); await userEvent.click(screen.getByLabelText('Loading results in the background')); - await userEvent.click(screen.getByText('Cancel')); + await userEvent.click(screen.getByText('Cancel session')); expect(onCancel).toBeCalled(); }); @@ -60,15 +60,15 @@ test('Loading in the background state', async () => { test('BackgroundCompleted state', async () => { render( - ); await userEvent.click(screen.getByLabelText('Results loaded in the background')); - expect(screen.getByRole('link', { name: 'View background sessions' }).getAttribute('href')).toBe( + expect(screen.getByRole('link', { name: 'View all sessions' }).getAttribute('href')).toBe( '__link__' ); }); @@ -77,7 +77,7 @@ test('Restored state', async () => { const onRefresh = jest.fn(); render( - + ); @@ -91,7 +91,7 @@ test('Canceled state', async () => { const onRefresh = jest.fn(); render( - + ); @@ -104,9 +104,9 @@ test('Canceled state', async () => { test('Disabled state', async () => { render( - + ); - expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx similarity index 57% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index ce77686c4f3c1..ed022e18c34d7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -20,31 +20,31 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import './background_session_indicator.scss'; -import { SessionState } from '../../../../../../../src/plugins/data/public/'; +import './search_session_indicator.scss'; +import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; -export interface BackgroundSessionIndicatorProps { - state: SessionState; +export interface SearchSessionIndicatorProps { + state: SearchSessionState; onContinueInBackground?: () => void; onCancel?: () => void; - viewBackgroundSessionsLink?: string; + viewSearchSessionsLink?: string; onSaveResults?: () => void; onRefresh?: () => void; disabled?: boolean; disabledReasonText?: string; } -type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; +type ActionButtonProps = SearchSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonProps) => ( ); @@ -55,28 +55,28 @@ const ContinueInBackgroundButton = ({ }: ActionButtonProps) => ( ); -const ViewBackgroundSessionsButton = ({ - viewBackgroundSessionsLink = 'management', +const ViewAllSearchSessionsButton = ({ + viewSearchSessionsLink = 'management', buttonProps = {}, }: ActionButtonProps) => ( ); @@ -84,11 +84,11 @@ const ViewBackgroundSessionsButton = ({ const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonProps) => ( @@ -97,18 +97,18 @@ const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonP const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( ); -const backgroundSessionIndicatorViewStateToProps: { - [state in SessionState]: { +const searchSessionIndicatorViewStateToProps: { + [state in SearchSessionState]: { button: Pick & { tooltipText: string; }; @@ -119,162 +119,151 @@ const backgroundSessionIndicatorViewStateToProps: { }; } | null; } = { - [SessionState.None]: null, - [SessionState.Loading]: { + [SearchSessionState.None]: null, + [SearchSessionState.Loading]: { button: { color: 'subdued', iconType: 'clock', 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingResultsIconAriaLabel', - { defaultMessage: 'Loading results' } + 'xpack.data.searchSessionIndicator.loadingResultsIconAriaLabel', + { defaultMessage: 'Loading' } ), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingResultsIconTooltipText', - { defaultMessage: 'Loading results' } + 'xpack.data.searchSessionIndicator.loadingResultsIconTooltipText', + { defaultMessage: 'Loading' } ), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.loadingResultsText', { + text: i18n.translate('xpack.data.searchSessionIndicator.loadingResultsText', { defaultMessage: 'Loading', }), primaryAction: CancelButton, secondaryAction: ContinueInBackgroundButton, }, }, - [SessionState.Completed]: { + [SearchSessionState.Completed]: { button: { color: 'subdued', iconType: 'checkInCircleFilled', - 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultsLoadedIconAriaLabel', - { - defaultMessage: 'Results loaded', - } - ), + 'aria-label': i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedIconAriaLabel', { + defaultMessage: 'Loaded', + }), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultsLoadedIconTooltipText', + 'xpack.data.searchSessionIndicator.resultsLoadedIconTooltipText', { defaultMessage: 'Results loaded', } ), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.resultsLoadedText', { - defaultMessage: 'Results loaded', + text: i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedText', { + defaultMessage: 'Loaded', }), primaryAction: SaveButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.BackgroundLoading]: { + [SearchSessionState.BackgroundLoading]: { button: { iconType: EuiLoadingSpinner, 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconAriaLabel', + 'xpack.data.searchSessionIndicator.loadingInTheBackgroundIconAriaLabel', { defaultMessage: 'Loading results in the background', } ), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconTooltipText', + 'xpack.data.searchSessionIndicator.loadingInTheBackgroundIconTooltipText', { defaultMessage: 'Loading results in the background', } ), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.loadingInTheBackgroundText', { + text: i18n.translate('xpack.data.searchSessionIndicator.loadingInTheBackgroundText', { defaultMessage: 'Loading in the background', }), primaryAction: CancelButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.BackgroundCompleted]: { + [SearchSessionState.BackgroundCompleted]: { button: { color: 'success', iconType: 'checkInCircleFilled', 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconAraText', + 'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundIconAraText', { defaultMessage: 'Results loaded in the background', } ), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconTooltipText', + 'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundIconTooltipText', { defaultMessage: 'Results loaded in the background', } ), }, popover: { - text: i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundText', - { - defaultMessage: 'Results loaded', - } - ), - primaryAction: ViewBackgroundSessionsButton, + text: i18n.translate('xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundText', { + defaultMessage: 'Loaded', + }), + primaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.Restored]: { + [SearchSessionState.Restored]: { button: { color: 'warning', iconType: 'refresh', 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.restoredResultsIconAriaLabel', - { - defaultMessage: 'Results no longer current', - } - ), - tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.restoredResultsTooltipText', + 'xpack.data.searchSessionIndicator.restoredResultsIconAriaLabel', { defaultMessage: 'Results no longer current', } ), + tooltipText: i18n.translate('xpack.data.searchSessionIndicator.restoredResultsTooltipText', { + defaultMessage: 'Results no longer current', + }), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.restoredText', { + text: i18n.translate('xpack.data.searchSessionIndicator.restoredText', { defaultMessage: 'Results no longer current', }), primaryAction: RefreshButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.Canceled]: { + [SearchSessionState.Canceled]: { button: { color: 'subdued', iconType: 'refresh', - 'aria-label': i18n.translate('xpack.data.backgroundSessionIndicator.canceledIconAriaLabel', { + 'aria-label': i18n.translate('xpack.data.searchSessionIndicator.canceledIconAriaLabel', { defaultMessage: 'Canceled', }), - tooltipText: i18n.translate('xpack.data.backgroundSessionIndicator.canceledTooltipText', { + tooltipText: i18n.translate('xpack.data.searchSessionIndicator.canceledTooltipText', { defaultMessage: 'Search was canceled', }), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.canceledText', { + text: i18n.translate('xpack.data.searchSessionIndicator.canceledText', { defaultMessage: 'Search was canceled', }), primaryAction: RefreshButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, }; -const VerticalDivider: React.FC = () => ( -
-); +const VerticalDivider: React.FC = () =>
; -export const BackgroundSessionIndicator: React.FC = (props) => { +export const SearchSessionIndicator: React.FC = (props) => { const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen); const closePopover = () => setIsPopoverOpen(false); - if (!backgroundSessionIndicatorViewStateToProps[props.state]) return null; + if (!searchSessionIndicatorViewStateToProps[props.state]) return null; - const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]!; + const { button, popover } = searchSessionIndicatorViewStateToProps[props.state]!; return ( @@ -302,8 +291,8 @@ export const BackgroundSessionIndicator: React.FC @@ -332,4 +321,4 @@ export const BackgroundSessionIndicator: React.FC { +export class EnhancedDataServerPlugin + implements Plugin { private readonly logger: Logger; - private sessionService!: BackgroundSessionService; + private sessionService!: SearchSessionService; constructor(private initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('data_enhanced'); @@ -38,7 +45,7 @@ export class EnhancedDataServerPlugin implements Plugin (id = response.id))); }, + + extend: async (id, keepAlive, options, { esClient }) => { + logger.debug(`_eql/extend ${id} by ${keepAlive}`); + await esClient.asCurrentUser.eql.get({ id, keep_alive: keepAlive }); + }, }; }; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index b9b6e25067f2f..3230895da7705 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -37,6 +37,7 @@ describe('ES search strategy', () => { const mockApiCaller = jest.fn(); const mockGetCaller = jest.fn(); const mockSubmitCaller = jest.fn(); + const mockDeleteCaller = jest.fn(); const mockLogger: any = { debug: () => {}, }; @@ -49,6 +50,7 @@ describe('ES search strategy', () => { asyncSearch: { get: mockGetCaller, submit: mockSubmitCaller, + delete: mockDeleteCaller, }, transport: { request: mockApiCaller }, }, @@ -66,77 +68,113 @@ describe('ES search strategy', () => { beforeEach(() => { mockApiCaller.mockClear(); + mockGetCaller.mockClear(); + mockSubmitCaller.mockClear(); + mockDeleteCaller.mockClear(); }); - it('returns a strategy with `search`', async () => { + it('returns a strategy with `search and `cancel`', async () => { const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); expect(typeof esSearch.search).toBe('function'); }); - it('makes a POST request to async search with params when no ID is provided', async () => { - mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + describe('search', () => { + it('makes a POST request to async search with params when no ID is provided', async () => { + mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); - const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search({ params }, {}, mockDeps).toPromise(); + await esSearch.search({ params }, {}, mockDeps).toPromise(); - expect(mockSubmitCaller).toBeCalled(); - const request = mockSubmitCaller.mock.calls[0][0]; - expect(request.index).toEqual(params.index); - expect(request.body).toEqual(params.body); - }); + expect(mockSubmitCaller).toBeCalled(); + const request = mockSubmitCaller.mock.calls[0][0]; + expect(request.index).toEqual(params.index); + expect(request.body).toEqual(params.body); + }); - it('makes a GET request to async search with ID when ID is provided', async () => { - mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); + it('makes a GET request to async search with ID when ID is provided', async () => { + mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); - const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); + + expect(mockGetCaller).toBeCalled(); + const request = mockGetCaller.mock.calls[0][0]; + expect(request.id).toEqual('foo'); + expect(request).toHaveProperty('wait_for_completion_timeout'); + expect(request).toHaveProperty('keep_alive'); + }); + + it('calls the rollup API if the index is a rollup type', async () => { + mockApiCaller.mockResolvedValueOnce(mockRollupResponse); + + const params = { index: 'foo-程', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch + .search( + { + indexType: 'rollup', + params, + }, + {}, + mockDeps + ) + .toPromise(); - await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); + expect(mockApiCaller).toBeCalled(); + const { method, path } = mockApiCaller.mock.calls[0][0]; + expect(method).toBe('POST'); + expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); + }); - expect(mockGetCaller).toBeCalled(); - const request = mockGetCaller.mock.calls[0][0]; - expect(request.id).toEqual('foo'); - expect(request).toHaveProperty('wait_for_completion_timeout'); - expect(request).toHaveProperty('keep_alive'); + it('sets wait_for_completion_timeout and keep_alive in the request', async () => { + mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'foo-*', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search({ params }, {}, mockDeps).toPromise(); + + expect(mockSubmitCaller).toBeCalled(); + const request = mockSubmitCaller.mock.calls[0][0]; + expect(request).toHaveProperty('wait_for_completion_timeout'); + expect(request).toHaveProperty('keep_alive'); + }); }); - it('calls the rollup API if the index is a rollup type', async () => { - mockApiCaller.mockResolvedValueOnce(mockRollupResponse); + describe('cancel', () => { + it('makes a DELETE request to async search with the provided ID', async () => { + mockDeleteCaller.mockResolvedValueOnce(200); - const params = { index: 'foo-程', body: {} }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const id = 'some_id'; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch - .search( - { - indexType: 'rollup', - params, - }, - {}, - mockDeps - ) - .toPromise(); - - expect(mockApiCaller).toBeCalled(); - const { method, path } = mockApiCaller.mock.calls[0][0]; - expect(method).toBe('POST'); - expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); + await esSearch.cancel!(id, {}, mockDeps); + + expect(mockDeleteCaller).toBeCalled(); + const request = mockDeleteCaller.mock.calls[0][0]; + expect(request).toEqual({ id }); + }); }); - it('sets wait_for_completion_timeout and keep_alive in the request', async () => { - mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + describe('extend', () => { + it('makes a GET request to async search with the provided ID and keepAlive', async () => { + mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); - const params = { index: 'foo-*', body: {} }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const id = 'some_other_id'; + const keepAlive = '1d'; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search({ params }, {}, mockDeps).toPromise(); + await esSearch.extend!(id, keepAlive, {}, mockDeps); - expect(mockSubmitCaller).toBeCalled(); - const request = mockSubmitCaller.mock.calls[0][0]; - expect(request).toHaveProperty('wait_for_completion_timeout'); - expect(request).toHaveProperty('keep_alive'); + expect(mockGetCaller).toBeCalled(); + const request = mockGetCaller.mock.calls[0][0]; + expect(request).toEqual({ id, keep_alive: keepAlive }); + }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index fc5787f9e5cab..c1520d931c272 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -111,5 +111,9 @@ export const enhancedEsSearchStrategyProvider = ( logger.debug(`cancel ${id}`); await esClient.asCurrentUser.asyncSearch.delete({ id }); }, + extend: async (id, keepAlive, options, { esClient }) => { + logger.debug(`extend ${id} by ${keepAlive}`); + await esClient.asCurrentUser.asyncSearch.get({ id, keep_alive: keepAlive }); + }, }; }; diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts new file mode 100644 index 0000000000000..4334ab3bc2903 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { checkRunningSessions } from './check_running_sessions'; +import { SearchSessionStatus, SearchSessionSavedObjectAttributes } from '../../../common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import type { SavedObjectsClientContract } from 'kibana/server'; +import { SearchStatus } from './types'; + +describe('getSearchStatus', () => { + let mockClient: any; + let savedObjectsClient: jest.Mocked; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + mockClient = { + asyncSearch: { + status: jest.fn(), + }, + }; + }); + + test('does nothing if there are no open sessions', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + } as any); + + await checkRunningSessions(savedObjectsClient, mockClient, mockLogger); + + expect(savedObjectsClient.bulkUpdate).not.toBeCalled(); + }); + + test('does nothing if there are no searchIds in the saved object', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { + attributes: { + idMapping: {}, + }, + }, + ], + total: 1, + } as any); + + await checkRunningSessions(savedObjectsClient, mockClient, mockLogger); + + expect(savedObjectsClient.bulkUpdate).not.toBeCalled(); + }); + + test('does nothing if the search is still running', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + }; + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [so], + total: 1, + } as any); + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: true, + is_running: true, + }, + }); + + await checkRunningSessions(savedObjectsClient, mockClient, mockLogger); + + expect(savedObjectsClient.bulkUpdate).not.toBeCalled(); + }); + + test("doesn't re-check completed or errored searches", async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.COMPLETE, + }, + 'another-search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.ERROR, + }, + }, + }, + }; + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [so], + total: 1, + } as any); + + await checkRunningSessions(savedObjectsClient, mockClient, mockLogger); + + expect(mockClient.asyncSearch.status).not.toBeCalled(); + }); + + test('updates to complete if the search is done', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + }; + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [so], + total: 1, + } as any); + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 200, + }, + }); + + await checkRunningSessions(savedObjectsClient, mockClient, mockLogger); + + expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' }); + const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; + const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes; + expect(updatedAttributes.status).toBe(SearchSessionStatus.COMPLETE); + expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.COMPLETE); + expect(updatedAttributes.idMapping['search-hash'].error).toBeUndefined(); + }); + + test('updates to error if the search is errored', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + }; + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [so], + total: 1, + } as any); + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 500, + }, + }); + + await checkRunningSessions(savedObjectsClient, mockClient, mockLogger); + const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; + + const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes; + expect(updatedAttributes.status).toBe(SearchSessionStatus.ERROR); + expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR); + expect(updatedAttributes.idMapping['search-hash'].error).toBe( + 'Search completed with a 500 status' + ); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts new file mode 100644 index 0000000000000..71274e15e284d --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Logger, + ElasticsearchClient, + SavedObjectsFindResult, + SavedObjectsClientContract, +} from 'kibana/server'; +import { + SearchSessionStatus, + SearchSessionSavedObjectAttributes, + SearchSessionRequestInfo, +} from '../../../common'; +import { SEARCH_SESSION_TYPE } from '../../saved_objects'; +import { getSearchStatus } from './get_search_status'; +import { getSessionStatus } from './get_session_status'; +import { SearchStatus } from './types'; + +export async function checkRunningSessions( + savedObjectsClient: SavedObjectsClientContract, + client: ElasticsearchClient, + logger: Logger +): Promise { + try { + const runningSearchSessionsResponse = await savedObjectsClient.find( + { + type: SEARCH_SESSION_TYPE, + search: SearchSessionStatus.IN_PROGRESS.toString(), + searchFields: ['status'], + namespaces: ['*'], + } + ); + + if (!runningSearchSessionsResponse.total) return; + + logger.debug(`Found ${runningSearchSessionsResponse.total} running sessions`); + + const updatedSessions = new Array>(); + + let sessionUpdated = false; + + await Promise.all( + runningSearchSessionsResponse.saved_objects.map(async (session) => { + // Check statuses of all running searches + await Promise.all( + Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { + const updateSearchRequest = ( + currentStatus: Pick + ) => { + sessionUpdated = true; + session.attributes.idMapping[searchKey] = { + ...session.attributes.idMapping[searchKey], + ...currentStatus, + }; + }; + + const searchInfo = session.attributes.idMapping[searchKey]; + if (searchInfo.status === SearchStatus.IN_PROGRESS) { + try { + const currentStatus = await getSearchStatus(client, searchInfo.id); + + if (currentStatus.status !== SearchStatus.IN_PROGRESS) { + updateSearchRequest(currentStatus); + } + } catch (e) { + logger.error(e); + updateSearchRequest({ + status: SearchStatus.ERROR, + error: e.message || e.meta.error?.caused_by?.reason, + }); + } + } + }) + ); + + // And only then derive the session's status + const sessionStatus = getSessionStatus(session.attributes); + if (sessionStatus !== SearchSessionStatus.IN_PROGRESS) { + session.attributes.status = sessionStatus; + sessionUpdated = true; + } + + if (sessionUpdated) { + updatedSessions.push(session); + } + }) + ); + + if (updatedSessions.length) { + // If there's an error, we'll try again in the next iteration, so there's no need to check the output. + const updatedResponse = await savedObjectsClient.bulkUpdate( + updatedSessions + ); + logger.debug(`Updated ${updatedResponse.saved_objects.length} background sessions`); + } + } catch (err) { + logger.error(err); + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/constants.ts b/x-pack/plugins/data_enhanced/server/search/session/constants.ts new file mode 100644 index 0000000000000..4ac32938c4843 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/constants.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. + */ + +export const INMEM_MAX_SESSIONS = 10000; +export const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; +export const INMEM_TRACKING_INTERVAL = 10 * 1000; +export const INMEM_TRACKING_TIMEOUT_SEC = 60; +export const MAX_UPDATE_RETRIES = 3; diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.test.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.test.ts new file mode 100644 index 0000000000000..e66ce613b71d9 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchStatus } from './types'; +import { getSearchStatus } from './get_search_status'; + +describe('getSearchStatus', () => { + let mockClient: any; + beforeEach(() => { + mockClient = { + asyncSearch: { + status: jest.fn(), + }, + }; + }); + + test('returns an error status if search is partial and not running', () => { + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: true, + is_running: false, + completion_status: 200, + }, + }); + expect(getSearchStatus(mockClient, '123')).resolves.toBe(SearchStatus.ERROR); + }); + + test('returns an error status if completion_status is an error', () => { + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 500, + }, + }); + expect(getSearchStatus(mockClient, '123')).resolves.toBe(SearchStatus.ERROR); + }); + + test('returns an error status if gets an ES error', () => { + mockClient.asyncSearch.status.mockResolvedValue({ + error: { + root_cause: { + reason: 'not found', + }, + }, + }); + expect(getSearchStatus(mockClient, '123')).resolves.toBe(SearchStatus.ERROR); + }); + + test('returns an error status throws', () => { + mockClient.asyncSearch.status.mockRejectedValue(new Error('O_o')); + expect(getSearchStatus(mockClient, '123')).resolves.toBe(SearchStatus.ERROR); + }); + + test('returns a complete status', () => { + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 200, + }, + }); + expect(getSearchStatus(mockClient, '123')).resolves.toBe(SearchStatus.COMPLETE); + }); + + test('returns a running status otherwise', () => { + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: true, + completion_status: undefined, + }, + }); + expect(getSearchStatus(mockClient, '123')).resolves.toBe(SearchStatus.IN_PROGRESS); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts new file mode 100644 index 0000000000000..e2b5fc0157b37 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.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. + */ + +import { i18n } from '@kbn/i18n'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { ElasticsearchClient } from 'src/core/server'; +import { SearchStatus } from './types'; +import { AsyncSearchStatusResponse } from '../types'; +import { SearchSessionRequestInfo } from '../../../common'; + +export async function getSearchStatus( + client: ElasticsearchClient, + asyncId: string +): Promise> { + // TODO: Handle strategies other than the default one + const apiResponse: ApiResponse = await client.asyncSearch.status({ + id: asyncId, + }); + const response = apiResponse.body; + if ((response.is_partial && !response.is_running) || response.completion_status >= 400) { + return { + status: SearchStatus.ERROR, + error: i18n.translate('xpack.data.search.statusError', { + defaultMessage: `Search completed with a {errorCode} status`, + values: { errorCode: response.completion_status }, + }), + }; + } else if (!response.is_partial && !response.is_running) { + return { + status: SearchStatus.COMPLETE, + error: undefined, + }; + } else { + return { + status: SearchStatus.IN_PROGRESS, + error: undefined, + }; + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_session_status.test.ts b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.test.ts new file mode 100644 index 0000000000000..35bfdeee691e2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchStatus } from './types'; +import { getSessionStatus } from './get_session_status'; +import { SearchSessionStatus } from '../../../common'; + +describe('getSessionStatus', () => { + test("returns an in_progress status if there's nothing inside the session", () => { + const session: any = { + idMapping: {}, + }; + expect(getSessionStatus(session)).toBe(SearchSessionStatus.IN_PROGRESS); + }); + + test("returns an error status if there's at least one error", () => { + const session: any = { + idMapping: { + a: { status: SearchStatus.IN_PROGRESS }, + b: { status: SearchStatus.ERROR, error: 'Nope' }, + c: { status: SearchStatus.COMPLETE }, + }, + }; + expect(getSessionStatus(session)).toBe(SearchSessionStatus.ERROR); + }); + + test('returns a complete status if all are complete', () => { + const session: any = { + idMapping: { + a: { status: SearchStatus.COMPLETE }, + b: { status: SearchStatus.COMPLETE }, + c: { status: SearchStatus.COMPLETE }, + }, + }; + expect(getSessionStatus(session)).toBe(SearchSessionStatus.COMPLETE); + }); + + test('returns a running status if some are still running', () => { + const session: any = { + idMapping: { + a: { status: SearchStatus.IN_PROGRESS }, + b: { status: SearchStatus.COMPLETE }, + c: { status: SearchStatus.IN_PROGRESS }, + }, + }; + expect(getSessionStatus(session)).toBe(SearchSessionStatus.IN_PROGRESS); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_session_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.ts new file mode 100644 index 0000000000000..296f4e489932d --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchSessionSavedObjectAttributes, SearchSessionStatus } from '../../../common'; +import { SearchStatus } from './types'; + +export function getSessionStatus(session: SearchSessionSavedObjectAttributes): SearchSessionStatus { + const searchStatuses = Object.values(session.idMapping); + if (searchStatuses.some((item) => item.status === SearchStatus.ERROR)) { + return SearchSessionStatus.ERROR; + } else if ( + searchStatuses.length > 0 && + searchStatuses.every((item) => item.status === SearchStatus.COMPLETE) + ) { + return SearchSessionStatus.COMPLETE; + } else { + return SearchSessionStatus.IN_PROGRESS; + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/index.ts b/x-pack/plugins/data_enhanced/server/search/session/index.ts index 5b75885fb31df..8d5e21f3d8276 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/index.ts @@ -5,3 +5,4 @@ */ export * from './session_service'; +export { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts new file mode 100644 index 0000000000000..a7d57c94fa153 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + TaskManagerSetupContract, + TaskManagerStartContract, + RunContext, +} from '../../../../task_manager/server'; +import { checkRunningSessions } from './check_running_sessions'; +import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; +import { SEARCH_SESSION_TYPE } from '../../saved_objects'; + +export const SEARCH_SESSIONS_TASK_TYPE = 'bg_monitor'; +export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; +export const MONITOR_INTERVAL = 15; // in seconds + +function searchSessionRunner(core: CoreSetup, logger: Logger) { + return ({ taskInstance }: RunContext) => { + return { + async run() { + const [coreStart] = await core.getStartServices(); + const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); + const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); + await checkRunningSessions( + internalSavedObjectsClient, + coreStart.elasticsearch.client.asInternalUser, + logger + ); + + return { + runAt: new Date(Date.now() + MONITOR_INTERVAL * 1000), + state: {}, + }; + }, + }; + }; +} + +export function registerSearchSessionsTask( + core: CoreSetup, + taskManager: TaskManagerSetupContract, + logger: Logger +) { + taskManager.registerTaskDefinitions({ + [SEARCH_SESSIONS_TASK_TYPE]: { + title: 'Search Sessions Monitor', + createTaskRunner: searchSessionRunner(core, logger), + }, + }); +} + +export async function scheduleSearchSessionsTasks( + taskManager: TaskManagerStartContract, + logger: Logger +) { + await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); + + try { + await taskManager.ensureScheduled({ + id: SEARCH_SESSIONS_TASK_ID, + taskType: SEARCH_SESSIONS_TASK_TYPE, + schedule: { + interval: `${MONITOR_INTERVAL}s`, + }, + state: {}, + params: {}, + }); + + logger.debug(`Background search task, scheduled to run`); + } catch (e) { + logger.debug(`Error scheduling task, received ${e.message}`); + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 766de908353f5..3114e746d0453 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -8,28 +8,27 @@ import { BehaviorSubject, of } from 'rxjs'; import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { BackgroundSessionStatus } from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { - BackgroundSessionDependencies, - BackgroundSessionService, - INMEM_TRACKING_INTERVAL, - MAX_UPDATE_RETRIES, - SessionInfo, -} from './session_service'; +import { SearchSessionStatus } from '../../../common'; +import { SEARCH_SESSION_TYPE } from '../../saved_objects'; +import { SearchSessionDependencies, SearchSessionService, SessionInfo } from './session_service'; import { createRequestHash } from './utils'; import moment from 'moment'; import { coreMock } from 'src/core/server/mocks'; import { ConfigSchema } from '../../../config'; +// @ts-ignore +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { INMEM_TRACKING_INTERVAL, MAX_UPDATE_RETRIES } from './constants'; +import { SearchStatus } from './types'; const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); -describe('BackgroundSessionService', () => { +describe('SearchSessionService', () => { let savedObjectsClient: jest.Mocked; - let service: BackgroundSessionService; + let service: SearchSessionService; const MOCK_SESSION_ID = 'session-id-mock'; const MOCK_ASYNC_ID = '123456'; + const MOCK_STRATEGY = 'ese'; const MOCK_KEY_HASH = '608de49a4600dbb5b173492759792e4a'; const createMockInternalSavedObjectClient = ( @@ -47,7 +46,10 @@ describe('BackgroundSessionService', () => { attributes: { sessionId: MOCK_SESSION_ID, idMapping: { - 'another-key': 'another-async-id', + 'another-key': { + id: 'another-async-id', + strategy: 'another-strategy', + }, }, }, id: MOCK_SESSION_ID, @@ -89,7 +91,7 @@ describe('BackgroundSessionService', () => { const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, attributes: { name: 'my_name', appId: 'my_app_id', @@ -106,7 +108,7 @@ describe('BackgroundSessionService', () => { warn: jest.fn(), error: jest.fn(), }; - service = new BackgroundSessionService(mockLogger); + service = new SearchSessionService(mockLogger); }); it('search throws if `name` is not provided', () => { @@ -127,7 +129,7 @@ describe('BackgroundSessionService', () => { const response = await service.get(sessionId, { savedObjectsClient }); expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); it('find calls saved objects client', async () => { @@ -149,7 +151,7 @@ describe('BackgroundSessionService', () => { expect(response).toBe(mockResponse); expect(savedObjectsClient.find).toHaveBeenCalledWith({ ...options, - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, }); }); @@ -165,7 +167,7 @@ describe('BackgroundSessionService', () => { expect(response).toBe(mockUpdateSavedObject); expect(savedObjectsClient.update).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, + SEARCH_SESSION_TYPE, sessionId, attributes ); @@ -177,14 +179,14 @@ describe('BackgroundSessionService', () => { const response = await service.delete(sessionId, { savedObjectsClient }); expect(response).toEqual({}); - expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); describe('search', () => { const mockSearch = jest.fn().mockReturnValue(of({})); const mockStrategy = { search: mockSearch }; const mockSearchDeps = {} as SearchStrategyDependencies; - const mockDeps = {} as BackgroundSessionDependencies; + const mockDeps = {} as SearchSessionDependencies; beforeEach(() => { mockSearch.mockClear(); @@ -283,7 +285,7 @@ describe('BackgroundSessionService', () => { await service.trackId( searchRequest, searchId, - { sessionId, isStored }, + { sessionId, isStored, strategy: MOCK_STRATEGY }, { savedObjectsClient } ); @@ -296,14 +298,14 @@ describe('BackgroundSessionService', () => { ); expect(savedObjectsClient.create).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, + SEARCH_SESSION_TYPE, { name, created, expires, initialState: {}, restoreState: {}, - status: BackgroundSessionStatus.IN_PROGRESS, + status: SearchSessionStatus.IN_PROGRESS, idMapping: {}, appId, urlGeneratorId, @@ -313,7 +315,8 @@ describe('BackgroundSessionService', () => { ); const [setSessionId, setParams] = setSpy.mock.calls[0]; - expect(setParams.ids.get(requestHash)).toBe(searchId); + expect(setParams.ids.get(requestHash).id).toBe(searchId); + expect(setParams.ids.get(requestHash).strategy).toBe(MOCK_STRATEGY); expect(setSessionId).toBe(sessionId); }); @@ -326,12 +329,18 @@ describe('BackgroundSessionService', () => { await service.trackId( searchRequest, searchId, - { sessionId, isStored }, + { sessionId, isStored, strategy: MOCK_STRATEGY }, { savedObjectsClient } ); - expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { - idMapping: { [requestHash]: searchId }, + expect(savedObjectsClient.update).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId, { + idMapping: { + [requestHash]: { + id: searchId, + strategy: MOCK_STRATEGY, + status: SearchStatus.IN_PROGRESS, + }, + }, }); }); }); @@ -375,12 +384,17 @@ describe('BackgroundSessionService', () => { const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const mockSession = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, attributes: { name: 'my_name', appId: 'my_app_id', urlGeneratorId: 'my_url_generator_id', - idMapping: { [requestHash]: searchId }, + idMapping: { + [requestHash]: { + id: searchId, + strategy: MOCK_STRATEGY, + }, + }, }, references: [], }; @@ -406,7 +420,11 @@ describe('BackgroundSessionService', () => { }, }, }); - await service.start(coreMock.createStart(), config$); + const mockTaskManager = taskManagerMock.createStart(); + await service.start(coreMock.createStart(), { + config$, + taskManager: mockTaskManager, + }); await flushPromises(); }); @@ -419,7 +437,10 @@ describe('BackgroundSessionService', () => { const findSpy = jest.fn().mockResolvedValue({ saved_objects: [] }); createMockInternalSavedObjectClient(findSpy); - const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]], moment()); + const mockIdMapping = createMockIdMapping( + [[MOCK_KEY_HASH, { id: MOCK_ASYNC_ID, strategy: MOCK_STRATEGY }]], + moment() + ); Object.defineProperty(service, 'sessionSearchMap', { get: () => mockIdMapping, @@ -438,7 +459,7 @@ describe('BackgroundSessionService', () => { createMockInternalSavedObjectClient(findSpy); const mockIdMapping = createMockIdMapping( - [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], + [[MOCK_KEY_HASH, { id: MOCK_ASYNC_ID, strategy: MOCK_STRATEGY }]], moment().subtract(2, 'm') ); @@ -459,7 +480,7 @@ describe('BackgroundSessionService', () => { createMockInternalSavedObjectClient(findSpy); const mockIdMapping = createMockIdMapping( - [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], + [[MOCK_KEY_HASH, { id: MOCK_ASYNC_ID, strategy: MOCK_STRATEGY }]], moment(), MAX_UPDATE_RETRIES ); @@ -528,7 +549,10 @@ describe('BackgroundSessionService', () => { attributes: { idMapping: { b: 'c', - [MOCK_KEY_HASH]: MOCK_ASYNC_ID, + [MOCK_KEY_HASH]: { + id: MOCK_ASYNC_ID, + strategy: MOCK_STRATEGY, + }, }, }, }, @@ -566,7 +590,10 @@ describe('BackgroundSessionService', () => { id: MOCK_SESSION_ID, attributes: { idMapping: { - b: 'c', + b: { + id: 'c', + strategy: MOCK_STRATEGY, + }, }, }, }, diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index d426e73b48510..8c9e0dad4957e 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -14,7 +14,9 @@ import { SavedObjectsClientContract, Logger, SavedObject, + CoreSetup, SavedObjectsBulkUpdateObject, + SavedObjectsFindOptions, } from '../../../../../../src/core/server'; import { IKibanaSearchRequest, @@ -30,31 +32,46 @@ import { SearchStrategyDependencies, } from '../../../../../../src/plugins/data/server'; import { - BackgroundSessionSavedObjectAttributes, - BackgroundSessionFindOptions, - BackgroundSessionStatus, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../task_manager/server'; +import { + SearchSessionSavedObjectAttributes, + SearchSessionRequestInfo, + SearchSessionStatus, } from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { SEARCH_SESSION_TYPE } from '../../saved_objects'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; - -const INMEM_MAX_SESSIONS = 10000; -const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; -export const INMEM_TRACKING_INTERVAL = 10 * 1000; -export const INMEM_TRACKING_TIMEOUT_SEC = 60; -export const MAX_UPDATE_RETRIES = 3; - -export interface BackgroundSessionDependencies { +import { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; +import { + DEFAULT_EXPIRATION, + INMEM_MAX_SESSIONS, + INMEM_TRACKING_INTERVAL, + INMEM_TRACKING_TIMEOUT_SEC, + MAX_UPDATE_RETRIES, +} from './constants'; +import { SearchStatus } from './types'; + +export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; } export interface SessionInfo { insertTime: Moment; retryCount: number; - ids: Map; + ids: Map; +} + +interface SetupDependencies { + taskManager: TaskManagerSetupContract; } -export class BackgroundSessionService implements ISessionService { +interface StartDependencies { + taskManager: TaskManagerStartContract; + config$: Observable; +} +export class SearchSessionService implements ISessionService { /** * Map of sessionId to { [requestHash]: searchId } * @private @@ -65,8 +82,12 @@ export class BackgroundSessionService implements ISessionService { constructor(private readonly logger: Logger) {} - public async start(core: CoreStart, config$: Observable) { - return this.setupMonitoring(core, config$); + public setup(core: CoreSetup, deps: SetupDependencies) { + registerSearchSessionsTask(core, deps.taskManager, this.logger); + } + + public async start(core: CoreStart, deps: StartDependencies) { + return this.setupMonitoring(core, deps); } public stop() { @@ -74,11 +95,12 @@ export class BackgroundSessionService implements ISessionService { clearTimeout(this.monitorTimer); } - private setupMonitoring = async (core: CoreStart, config$: Observable) => { - const config = await config$.pipe(first()).toPromise(); + private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => { + const config = await deps.config$.pipe(first()).toPromise(); if (config.search.sendToBackground.enabled) { + scheduleSearchSessionsTasks(deps.taskManager, this.logger); this.logger.debug(`setupMonitoring | Enabling monitoring`); - const internalRepo = core.savedObjects.createInternalRepository([BACKGROUND_SESSION_TYPE]); + const internalRepo = core.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); this.monitorMappedIds(); } @@ -91,7 +113,7 @@ export class BackgroundSessionService implements ISessionService { private sessionIdsAsFilters(sessionIds: string[]): KueryNode { return nodeBuilder.or( sessionIds.map((id) => { - return nodeBuilder.is(`${BACKGROUND_SESSION_TYPE}.attributes.sessionId`, id); + return nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.sessionId`, id); }) ); } @@ -106,9 +128,9 @@ export class BackgroundSessionService implements ISessionService { */ private async getAllMappedSavedObjects() { const filter = this.sessionIdsAsFilters(Array.from(this.sessionSearchMap.keys())); - const res = await this.internalSavedObjectsClient.find({ + const res = await this.internalSavedObjectsClient.find({ perPage: INMEM_MAX_SESSIONS, // If there are more sessions in memory, they will be synced when some items are cleared out. - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, filter, namespaces: ['*'], }); @@ -174,13 +196,13 @@ export class BackgroundSessionService implements ISessionService { } private async updateAllSavedObjects( - activeMappingObjects: Array> + activeMappingObjects: Array> ) { if (!activeMappingObjects.length) return []; this.logger.debug(`updateAllSavedObjects | Updating ${activeMappingObjects.length} items`); const updatedSessions: Array< - SavedObjectsBulkUpdateObject + SavedObjectsBulkUpdateObject > = activeMappingObjects .filter((so) => !so.error) .map((sessionSavedObject) => { @@ -196,7 +218,7 @@ export class BackgroundSessionService implements ISessionService { }; }); - const updateResults = await this.internalSavedObjectsClient.bulkUpdate( + const updateResults = await this.internalSavedObjectsClient.bulkUpdate( updatedSessions ); return updateResults.saved_objects; @@ -207,7 +229,7 @@ export class BackgroundSessionService implements ISessionService { searchRequest: Request, options: ISearchOptions, searchDeps: SearchStrategyDependencies, - deps: BackgroundSessionDependencies + deps: SearchSessionDependencies ): Observable { // If this is a restored background search session, look up the ID using the provided sessionId const getSearchRequest = async () => @@ -235,12 +257,12 @@ export class BackgroundSessionService implements ISessionService { appId, created = new Date().toISOString(), expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), - status = BackgroundSessionStatus.IN_PROGRESS, + status = SearchSessionStatus.IN_PROGRESS, urlGeneratorId, initialState = {}, restoreState = {}, - }: Partial, - { savedObjectsClient }: BackgroundSessionDependencies + }: Partial, + { savedObjectsClient }: SearchSessionDependencies ) => { if (!name) throw new Error('Name is required'); if (!appId) throw new Error('AppId is required'); @@ -260,8 +282,8 @@ export class BackgroundSessionService implements ISessionService { appId, sessionId, }; - const session = await savedObjectsClient.create( - BACKGROUND_SESSION_TYPE, + const session = await savedObjectsClient.create( + SEARCH_SESSION_TYPE, attributes, { id: sessionId } ); @@ -270,42 +292,42 @@ export class BackgroundSessionService implements ISessionService { }; // TODO: Throw an error if this session doesn't belong to this user - public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + public get = (sessionId: string, { savedObjectsClient }: SearchSessionDependencies) => { this.logger.debug(`get | ${sessionId}`); - return savedObjectsClient.get( - BACKGROUND_SESSION_TYPE, + return savedObjectsClient.get( + SEARCH_SESSION_TYPE, sessionId ); }; // TODO: Throw an error if this session doesn't belong to this user public find = ( - options: BackgroundSessionFindOptions, - { savedObjectsClient }: BackgroundSessionDependencies + options: Omit, + { savedObjectsClient }: SearchSessionDependencies ) => { - return savedObjectsClient.find({ + return savedObjectsClient.find({ ...options, - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, }); }; // TODO: Throw an error if this session doesn't belong to this user public update = ( sessionId: string, - attributes: Partial, - { savedObjectsClient }: BackgroundSessionDependencies + attributes: Partial, + { savedObjectsClient }: SearchSessionDependencies ) => { this.logger.debug(`update | ${sessionId}`); - return savedObjectsClient.update( - BACKGROUND_SESSION_TYPE, + return savedObjectsClient.update( + SEARCH_SESSION_TYPE, sessionId, attributes ); }; // TODO: Throw an error if this session doesn't belong to this user - public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + public delete = (sessionId: string, { savedObjectsClient }: SearchSessionDependencies) => { + return savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); }; /** @@ -316,25 +338,32 @@ export class BackgroundSessionService implements ISessionService { public trackId = async ( searchRequest: IKibanaSearchRequest, searchId: string, - { sessionId, isStored }: ISearchOptions, - deps: BackgroundSessionDependencies + { sessionId, isStored, strategy }: ISearchOptions, + deps: SearchSessionDependencies ) => { if (!sessionId || !searchId) return; this.logger.debug(`trackId | ${sessionId} | ${searchId}`); const requestHash = createRequestHash(searchRequest.params); + const searchInfo = { + id: searchId, + strategy: strategy!, + status: SearchStatus.IN_PROGRESS, + }; // If there is already a saved object for this session, update it to include this request/ID. // Otherwise, just update the in-memory mapping for this session for when the session is saved. if (isStored) { - const attributes = { idMapping: { [requestHash]: searchId } }; + const attributes = { + idMapping: { [requestHash]: searchInfo }, + }; await this.update(sessionId, attributes, deps); } else { const map = this.sessionSearchMap.get(sessionId) ?? { insertTime: moment(), retryCount: 0, - ids: new Map(), + ids: new Map(), }; - map.ids.set(requestHash, searchId); + map.ids.set(requestHash, searchInfo); this.sessionSearchMap.set(sessionId, map); } }; @@ -347,7 +376,7 @@ export class BackgroundSessionService implements ISessionService { public getId = async ( searchRequest: IKibanaSearchRequest, { sessionId, isStored, isRestore }: ISearchOptions, - deps: BackgroundSessionDependencies + deps: SearchSessionDependencies ) => { if (!sessionId) { throw new Error('Session ID is required'); @@ -363,13 +392,13 @@ export class BackgroundSessionService implements ISessionService { throw new Error('No search ID in this session matching the given search request'); } - return session.attributes.idMapping[requestHash]; + return session.attributes.idMapping[requestHash].id; }; public asScopedProvider = ({ savedObjects }: CoreStart) => { return (request: KibanaRequest) => { const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + includedHiddenTypes: [SEARCH_SESSION_TYPE], }); const deps = { savedObjectsClient }; return { @@ -377,11 +406,11 @@ export class BackgroundSessionService implements ISessionService { strategy: ISearchStrategy, ...args: Parameters['search']> ) => this.search(strategy, ...args, deps), - save: (sessionId: string, attributes: Partial) => + save: (sessionId: string, attributes: Partial) => this.save(sessionId, attributes, deps), get: (sessionId: string) => this.get(sessionId, deps), - find: (options: BackgroundSessionFindOptions) => this.find(options, deps), - update: (sessionId: string, attributes: Partial) => + find: (options: SavedObjectsFindOptions) => this.find(options, deps), + update: (sessionId: string, attributes: Partial) => this.update(sessionId, attributes, deps), delete: (sessionId: string) => this.delete(sessionId, deps), }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/legacy_event_index_pattern.ts b/x-pack/plugins/data_enhanced/server/search/session/types.ts similarity index 66% rename from x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/legacy_event_index_pattern.ts rename to x-pack/plugins/data_enhanced/server/search/session/types.ts index 01e818c89b3ef..c30e03f70d2dc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/legacy_event_index_pattern.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/types.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * Legacy events are stored in indices with endgame-* prefix - */ -export const legacyEventIndexPattern = 'endgame-*'; +export enum SearchStatus { + IN_PROGRESS = 'in_progress', + ERROR = 'error', + COMPLETE = 'complete', +} diff --git a/x-pack/plugins/data_enhanced/server/search/types.ts b/x-pack/plugins/data_enhanced/server/search/types.ts index f01ac51a1516e..4401b7211fb62 100644 --- a/x-pack/plugins/data_enhanced/server/search/types.ts +++ b/x-pack/plugins/data_enhanced/server/search/types.ts @@ -4,14 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; +import { SearchResponse, ShardsResponse } from 'elasticsearch'; export interface AsyncSearchResponse { id?: string; response: SearchResponse; + start_time_in_millis: number; + expiration_time_in_millis: number; is_partial: boolean; is_running: boolean; } +export interface AsyncSearchStatusResponse extends Omit { + completion_status: number; + _shards: ShardsResponse; +} export interface EqlSearchResponse extends SearchResponse { id?: string; diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json new file mode 100644 index 0000000000000..ec5c656ac50b5 --- /dev/null +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "config.ts", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "public/autocomplete/providers/kql_query_suggestion/__fixtures__/*.json" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/bfetch/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + + { "path": "../features/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/discover_enhanced/public/index.ts b/x-pack/plugins/discover_enhanced/public/index.ts index 943a212dd7c4e..c44815707b9be 100644 --- a/x-pack/plugins/discover_enhanced/public/index.ts +++ b/x-pack/plugins/discover_enhanced/public/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'kibana/public'; +import type { PluginInitializerContext } from 'kibana/public'; import { DiscoverEnhancedPlugin } from './plugin'; -export { +export type { ExploreDataContextMenuAction, ExploreDataChartAction } from './actions'; + +export type { DiscoverEnhancedPlugin, DiscoverEnhancedSetupDependencies, DiscoverEnhancedStartDependencies, diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index 78f3464484ccf..7f6f83fba16f6 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -15,25 +15,11 @@ import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../src/plugins/ki import { EmbeddableSetup, EmbeddableStart, - EmbeddableContext, CONTEXT_MENU_TRIGGER, } from '../../../../src/plugins/embeddable/public'; -import { - ExploreDataContextMenuAction, - ExploreDataChartAction, - ACTION_EXPLORE_DATA, - ACTION_EXPLORE_DATA_CHART, - ExploreDataChartActionContext, -} from './actions'; +import { ExploreDataContextMenuAction, ExploreDataChartAction } from './actions'; import { Config } from '../common'; -declare module '../../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [ACTION_EXPLORE_DATA]: EmbeddableContext; - [ACTION_EXPLORE_DATA_CHART]: ExploreDataChartActionContext; - } -} - export interface DiscoverEnhancedSetupDependencies { discover: DiscoverSetup; embeddable: EmbeddableSetup; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index e3730084d7020..1c6d7e4066187 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -443,3 +443,77 @@ describe('UrlDrilldown', () => { }); }); }); + +describe('encoding', () => { + const urlDrilldown = createDrilldown(); + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + test('encodes URL by default', async () => { + const config: Config = { + url: { + template: 'https://elastic.co?foo=head%26shoulders', + }, + openInNewTab: false, + }; + const url = await urlDrilldown.getHref(config, context); + + expect(url).toBe('https://elastic.co?foo=head%2526shoulders'); + }); + + test('encodes URL when encoding is enabled', async () => { + const config: Config = { + url: { + template: 'https://elastic.co?foo=head%26shoulders', + }, + openInNewTab: false, + encodeUrl: true, + }; + const url = await urlDrilldown.getHref(config, context); + + expect(url).toBe('https://elastic.co?foo=head%2526shoulders'); + }); + + test('does not encode URL when encoding is not enabled', async () => { + const config: Config = { + url: { + template: 'https://elastic.co?foo=head%26shoulders', + }, + openInNewTab: false, + encodeUrl: false, + }; + const url = await urlDrilldown.getHref(config, context); + + expect(url).toBe('https://elastic.co?foo=head%26shoulders'); + }); + + test('can encode URI component using "encodeURIComponent" Handlebars helper', async () => { + const config: Config = { + url: { + template: 'https://elastic.co?foo={{encodeURIComponent "head%26shoulders@gmail.com"}}', + }, + openInNewTab: false, + encodeUrl: false, + }; + const url = await urlDrilldown.getHref(config, context); + + expect(url).toBe('https://elastic.co?foo=head%2526shoulders%40gmail.com'); + }); + + test('can encode URI component using "encodeURIQuery" Handlebars helper', async () => { + const config: Config = { + url: { + template: 'https://elastic.co?foo={{encodeURIQuery "head%26shoulders@gmail.com"}}', + }, + openInNewTab: false, + encodeUrl: false, + }; + const url = await urlDrilldown.getHref(config, context); + + expect(url).toBe('https://elastic.co?foo=head%2526shoulders@gmail.com'); + }); +}); diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx index bfeab263d20e3..3587e472324c6 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx @@ -56,14 +56,14 @@ export type UrlTrigger = | typeof ROW_CLICK_TRIGGER | typeof CONTEXT_MENU_TRIGGER; -export interface ActionFactoryContext extends BaseActionFactoryContext { +export interface ActionFactoryContext extends BaseActionFactoryContext { embeddable?: EmbeddableWithQueryInput; } export type CollectConfigProps = CollectConfigPropsBase; const URL_DRILLDOWN = 'URL_DRILLDOWN'; -export class UrlDrilldown implements Drilldown { +export class UrlDrilldown implements Drilldown { public readonly id = URL_DRILLDOWN; constructor(private readonly deps: UrlDrilldownDeps) {} @@ -104,7 +104,8 @@ export class UrlDrilldown implements Drilldown ({ url: { template: '' }, - openInNewTab: false, + openInNewTab: true, + encodeUrl: true, }); public readonly isConfigValid = (config: Config): config is Config => { @@ -133,7 +134,12 @@ export class UrlDrilldown implements Drilldown ({ })); /** + * React component helpers + * * Call this function to override a specific set of Kea values while retaining all other defaults - * Example usage within a component test: * - * import '../../../__mocks__/kea'; - * import { setMockValues } from ''../../../__mocks__'; + * Example usage: + * + * import { setMockValues } from '../../../__mocks__/kea.mock'; + * import { SomeComponent } from './'; * * it('some test', () => { * setMockValues({ someValue: 'hello' }); + * shallow(); * }); */ import { useValues, useActions } from 'kea'; @@ -58,3 +62,62 @@ export const setMockValues = (values: object) => { export const setMockActions = (actions: object) => { (useActions as jest.Mock).mockImplementation(() => ({ ...mockAllActions, ...actions })); }; + +/** + * Kea logic helpers + * + * Call this function to mount a logic file and optionally override default values. + * Automatically DRYs out a lot of cruft for us, such as resetting context, creating the + * nested defaults path obj (see https://kea.js.org/docs/api/context#resetcontext), and + * returning an unmount function + * + * Example usage: + * + * import { LogicMounter } from '../../../__mocks__/kea.mock'; + * import { SomeLogic } from './'; + * + * const { mount, unmount } = new LogicMounter(SomeLogic); + * + * it('some test', () => { + * mount({ someValue: 'hello' }); + * unmount(); + * }); + */ +import { resetContext, Logic, LogicInput } from 'kea'; + +interface LogicFile { + inputs: Array>; + mount(): Function; +} +export class LogicMounter { + private logicFile: LogicFile; + private unmountFn!: Function; + + constructor(logicFile: LogicFile) { + this.logicFile = logicFile; + } + + // Reset context with optional default value overrides + public resetContext = (values?: object) => { + if (!values) { + resetContext({}); + } else { + const path = this.logicFile.inputs[0].path as string[]; // example: ['x', 'y', 'z'] + const defaults = path.reduceRight((value: object, key: string) => ({ [key]: value }), values); // example: { x: { y: { z: values } } } + resetContext({ defaults }); + } + }; + + // Automatically reset context & mount the logic file + public mount = (values?: object) => { + this.resetContext(values); + const unmount = this.logicFile.mount(); + this.unmountFn = unmount; + return unmount; // Keep Kea behavior of returning an unmount fn from mount + }; + + // Also add unmount as a class method that can be destructured on init without becoming stale later + public unmount = () => { + this.unmountFn(); + }; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts index 51ae11ad2ab82..1e25582e3d569 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts @@ -6,6 +6,11 @@ import { i18n } from '@kbn/i18n'; +export const ANALYTICS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.title', + { defaultMessage: 'Analytics' } +); + export const TOTAL_DOCUMENTS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.totalDocuments', { defaultMessage: 'Total documents' } diff --git a/x-pack/plugins/logstash/server/routes/upgrade/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts similarity index 82% rename from x-pack/plugins/logstash/server/routes/upgrade/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts index 3a5b0868b446b..7cddef645e838 100644 --- a/x-pack/plugins/logstash/server/routes/upgrade/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerUpgradeRoute } from './upgrade'; +export { ANALYTICS_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts index 6fd60b7a34ebc..0a12f872f18f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts @@ -6,6 +6,11 @@ import { i18n } from '@kbn/i18n'; +export const API_LOGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.apiLogs.title', + { defaultMessage: 'API Logs' } +); + export const RECENT_API_EVENTS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.apiLogs.recent', { defaultMessage: 'Recent API events' } diff --git a/x-pack/plugins/logstash/public/application/components/upgrade_failure/index.js b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts similarity index 82% rename from x-pack/plugins/logstash/public/application/components/upgrade_failure/index.js rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts index 0aa757bca5236..72cd9efa887c3 100644 --- a/x-pack/plugins/logstash/public/application/components/upgrade_failure/index.js +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { UpgradeFailure } from './upgrade_failure'; +export { API_LOGS_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/constants.ts new file mode 100644 index 0000000000000..777d75600a279 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CRAWLER_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.crawler.title', + { defaultMessage: 'Crawler' } +); diff --git a/x-pack/plugins/logstash/public/services/upgrade/index.js b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/index.ts old mode 100755 new mode 100644 similarity index 82% rename from x-pack/plugins/logstash/public/services/upgrade/index.js rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/index.ts index 1c835b11ae423..f5b81c318ed56 --- a/x-pack/plugins/logstash/public/services/upgrade/index.js +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { UpgradeService } from './upgrade_service'; +export { CRAWLER_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx index 4f5ded0a3ccc1..ec018f0faf5ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -204,7 +204,11 @@ describe('Credentials', () => { copy: expect.any(Function), toggleIsHidden: expect.any(Function), isHidden: expect.any(Boolean), - text: •••••••, + text: ( + + ••••••• + + ), }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 9240bade4975e..df85a9c3053a6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -40,6 +40,7 @@ export const CredentialsList: React.FC = () => { { name: 'Key', width: '36%', + className: 'eui-textBreakAll', render: (token: ApiToken) => { const { key } = token; if (!key) return null; @@ -60,6 +61,10 @@ export const CredentialsList: React.FC = () => { ); }, + mobileOptions: { + // @ts-ignore - EUI's type definitions need to be updated + width: '100%', + }, }, { name: 'Modes', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx index fa2d124cbccdf..8ea2b6c284fc6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx @@ -39,6 +39,7 @@ export const Key: React.FC = ({ copy, toggleIsHidden, isHidden, text }) = iconType={hideIcon} aria-label={hideIconLabel} aria-pressed={!isHidden} + style={{ marginRight: '0.25em' }} /> {text} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 6523b4fb110b0..48be2b0ae8dfd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -57,24 +57,7 @@ describe('CredentialsLogic', () => { fullEngineAccessChecked: false, }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - credentials_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - CredentialsLogic.mount(); - }; + const { mount } = new LogicMounter(CredentialsLogic); const newToken = { id: 1, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts new file mode 100644 index 0000000000000..3b88d2c4983d4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CURATIONS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.title', + { defaultMessage: 'Curations' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/index.ts new file mode 100644 index 0000000000000..ecb9c223e4b6e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CURATIONS_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx index 27c3410767d8a..c685ed8863985 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx @@ -16,6 +16,34 @@ export const FLYOUT_CONTINUE_BUTTON = i18n.translate( 'xpack.enterpriseSearch.appSearch.documentCreation.flyoutContinue', { defaultMessage: 'Continue' } ); +export const FLYOUT_CLOSE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.modalClose', + { defaultMessage: 'Close' } +); + +export const DOCUMENT_CREATION_ERRORS = { + TITLE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.errorsTitle', { + defaultMessage: 'Something went wrong. Please address the errors and try again.', + }), + NO_FILE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.noFileFound', { + defaultMessage: 'No file found.', + }), + NO_VALID_FILE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.noValidFile', { + defaultMessage: 'Problem parsing file.', + }), + NOT_VALID: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.notValidJson', { + defaultMessage: 'Document contents must be a valid JSON array or object.', + }), +}; +export const DOCUMENT_CREATION_WARNINGS = { + TITLE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.warningsTitle', { + defaultMessage: 'Warning!', + }), + LARGE_FILE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.largeFile', { + defaultMessage: + "You're uploading an extremely large file. This could potentially lock your browser, or take a very long time to process. If possible, try splitting your data up into multiple smaller files.", + }), +}; // This is indented the way it is to work with ApiCodeExample. // Use dedent() when calling this alone diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx index 9ebe404659ca2..c33cda9f7e429 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx @@ -23,6 +23,8 @@ import { EuiBadge, EuiCode, EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; @@ -95,8 +97,14 @@ export const FlyoutBody: React.FC = () => { - POST - {documentsApiUrl} + + + POST + + + {documentsApiUrl} + + {dedent(` diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx index 50e4d473e5f78..39c6abcaab7b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx @@ -11,11 +11,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiTextArea, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import { Errors } from '../creation_response_components'; import { PasteJsonText, FlyoutHeader, FlyoutBody, FlyoutFooter } from './paste_json_text'; describe('PasteJsonText', () => { const values = { textInput: 'hello world', + isUploading: false, + errors: [], configuredLimits: { engine: { maxDocumentByteSize: 102400, @@ -24,6 +27,7 @@ describe('PasteJsonText', () => { }; const actions = { setTextInput: jest.fn(), + onSubmitJson: jest.fn(), closeDocumentCreation: jest.fn(), }; @@ -58,6 +62,16 @@ describe('PasteJsonText', () => { textarea.simulate('change', { target: { value: 'dolor sit amet' } }); expect(actions.setTextInput).toHaveBeenCalledWith('dolor sit amet'); }); + + it('shows an error banner and sets invalid form props if errors exist', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiTextArea).prop('isInvalid')).toBe(false); + + setMockValues({ ...values, errors: ['some error'] }); + rerender(wrapper); + expect(wrapper.find(EuiTextArea).prop('isInvalid')).toBe(true); + expect(wrapper.prop('banner').type).toEqual(Errors); + }); }); describe('FlyoutFooter', () => { @@ -68,6 +82,13 @@ describe('PasteJsonText', () => { expect(actions.closeDocumentCreation).toHaveBeenCalled(); }); + it('submits json', () => { + const wrapper = shallow(); + + wrapper.find(EuiButton).simulate('click'); + expect(actions.onSubmitJson).toHaveBeenCalled(); + }); + it('disables/enables the Continue button based on whether text has been entered', () => { const wrapper = shallow(); expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(false); @@ -76,5 +97,14 @@ describe('PasteJsonText', () => { rerender(wrapper); expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true); }); + + it('sets isLoading based on isUploading', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButton).prop('isLoading')).toBe(false); + + setMockValues({ ...values, isUploading: true }); + rerender(wrapper); + expect(wrapper.find(EuiButton).prop('isLoading')).toBe(true); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx index ad83e0eb1a286..b1f83095f30af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx @@ -25,6 +25,7 @@ import { import { AppLogic } from '../../../app_logic'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants'; +import { Errors } from '../creation_response_components'; import { DocumentCreationLogic } from '../'; import './paste_json_text.scss'; @@ -55,11 +56,11 @@ export const FlyoutBody: React.FC = () => { const { configuredLimits } = useValues(AppLogic); const maxDocumentByteSize = configuredLimits?.engine?.maxDocumentByteSize; - const { textInput } = useValues(DocumentCreationLogic); + const { textInput, errors } = useValues(DocumentCreationLogic); const { setTextInput } = useActions(DocumentCreationLogic); return ( - + }>

{i18n.translate( @@ -76,6 +77,7 @@ export const FlyoutBody: React.FC = () => { setTextInput(e.target.value)} + isInvalid={errors.length > 0} aria-label={i18n.translate( 'xpack.enterpriseSearch.appSearch.documentCreation.pasteJsonText.label', { defaultMessage: 'Paste JSON here' } @@ -89,8 +91,8 @@ export const FlyoutBody: React.FC = () => { }; export const FlyoutFooter: React.FC = () => { - const { textInput } = useValues(DocumentCreationLogic); - const { closeDocumentCreation } = useActions(DocumentCreationLogic); + const { textInput, isUploading } = useValues(DocumentCreationLogic); + const { onSubmitJson, closeDocumentCreation } = useActions(DocumentCreationLogic); return ( @@ -99,7 +101,7 @@ export const FlyoutFooter: React.FC = () => { {FLYOUT_CANCEL_BUTTON} - + {FLYOUT_CONTINUE_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx index 72a245df817ba..a5cb1885d9a04 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx @@ -3,11 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import { rerender } from '../../../../__mocks__'; @@ -16,12 +11,15 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiFilePicker, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import { Errors } from '../creation_response_components'; import { UploadJsonFile, FlyoutHeader, FlyoutBody, FlyoutFooter } from './upload_json_file'; describe('UploadJsonFile', () => { const mockFile = new File(['mock'], 'mock.json', { type: 'application/json' }); const values = { fileInput: null, + isUploading: false, + errors: [], configuredLimits: { engine: { maxDocumentByteSize: 102400, @@ -30,6 +28,7 @@ describe('UploadJsonFile', () => { }; const actions = { setFileInput: jest.fn(), + onSubmitFile: jest.fn(), closeDocumentCreation: jest.fn(), }; @@ -63,6 +62,25 @@ describe('UploadJsonFile', () => { wrapper.find(EuiFilePicker).simulate('change', []); expect(actions.setFileInput).toHaveBeenCalledWith(null); }); + + it('sets isLoading based on isUploading', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFilePicker).prop('isLoading')).toBe(false); + + setMockValues({ ...values, isUploading: true }); + rerender(wrapper); + expect(wrapper.find(EuiFilePicker).prop('isLoading')).toBe(true); + }); + + it('shows an error banner and sets invalid form props if errors exist', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFilePicker).prop('isInvalid')).toBe(false); + + setMockValues({ ...values, errors: ['some error'] }); + rerender(wrapper); + expect(wrapper.find(EuiFilePicker).prop('isInvalid')).toBe(true); + expect(wrapper.prop('banner').type).toEqual(Errors); + }); }); describe('FlyoutFooter', () => { @@ -73,6 +91,13 @@ describe('UploadJsonFile', () => { expect(actions.closeDocumentCreation).toHaveBeenCalled(); }); + it('submits the json file', () => { + const wrapper = shallow(); + + wrapper.find(EuiButton).simulate('click'); + expect(actions.onSubmitFile).toHaveBeenCalled(); + }); + it('disables/enables the Continue button based on whether files have been uploaded', () => { const wrapper = shallow(); expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true); @@ -81,5 +106,14 @@ describe('UploadJsonFile', () => { rerender(wrapper); expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true); }); + + it('sets isLoading based on isUploading', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButton).prop('isLoading')).toBe(false); + + setMockValues({ ...values, isUploading: true }); + rerender(wrapper); + expect(wrapper.find(EuiButton).prop('isLoading')).toBe(true); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx index 6c5b1de79c320..86841223c7255 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx @@ -3,11 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * 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 { useValues, useActions } from 'kea'; @@ -30,6 +25,7 @@ import { import { AppLogic } from '../../../app_logic'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants'; +import { Errors } from '../creation_response_components'; import { DocumentCreationLogic } from '../'; export const UploadJsonFile: React.FC = () => ( @@ -59,10 +55,11 @@ export const FlyoutBody: React.FC = () => { const { configuredLimits } = useValues(AppLogic); const maxDocumentByteSize = configuredLimits?.engine?.maxDocumentByteSize; + const { isUploading, errors } = useValues(DocumentCreationLogic); const { setFileInput } = useActions(DocumentCreationLogic); return ( - + }>

{i18n.translate( @@ -80,14 +77,16 @@ export const FlyoutBody: React.FC = () => { onChange={(files) => setFileInput(files?.length ? files[0] : null)} accept="application/json" fullWidth + isLoading={isUploading} + isInvalid={errors.length > 0} /> ); }; export const FlyoutFooter: React.FC = () => { - const { fileInput } = useValues(DocumentCreationLogic); - const { closeDocumentCreation } = useActions(DocumentCreationLogic); + const { fileInput, isUploading } = useValues(DocumentCreationLogic); + const { onSubmitFile, closeDocumentCreation } = useActions(DocumentCreationLogic); return ( @@ -96,7 +95,7 @@ export const FlyoutFooter: React.FC = () => { {FLYOUT_CANCEL_BUTTON} - + {FLYOUT_CONTINUE_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx new file mode 100644 index 0000000000000..ec73184621b53 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.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 { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + +import { Errors } from './'; + +describe('Errors', () => { + it('does not render if no errors or warnings to render', () => { + setMockValues({ errors: [], warnings: [] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(0); + }); + + it('renders errors', () => { + setMockValues({ errors: ['error 1', 'error 2'], warnings: [] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(EuiCallOut).prop('title')).toEqual( + 'Something went wrong. Please address the errors and try again.' + ); + expect(wrapper.find('p').first().text()).toEqual('error 1'); + expect(wrapper.find('p').last().text()).toEqual('error 2'); + }); + + it('renders warnings', () => { + setMockValues({ errors: [], warnings: ['document size warning'] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(EuiCallOut).prop('title')).toEqual('Warning!'); + expect(wrapper.find('p').text()).toEqual('document size warning'); + }); + + it('renders both errors and warnings', () => { + setMockValues({ errors: ['some error'], warnings: ['some warning'] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx new file mode 100644 index 0000000000000..cf0c4e1c46a13 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.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 { useValues } from 'kea'; + +import { EuiCallOut } from '@elastic/eui'; + +import { DOCUMENT_CREATION_ERRORS, DOCUMENT_CREATION_WARNINGS } from '../constants'; +import { DocumentCreationLogic } from '../'; + +export const Errors: React.FC = () => { + const { errors, warnings } = useValues(DocumentCreationLogic); + + return ( + <> + {errors.length > 0 && ( + + {errors.map((message, index) => ( +

{message}

+ ))} + + )} + {warnings.length > 0 && ( + + {warnings.map((message, index) => ( +

{message}

+ ))} +
+ )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/index.ts new file mode 100644 index 0000000000000..eb4aec46d1f08 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Errors } from './errors'; +export { Summary } from './summary'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx new file mode 100644 index 0000000000000..9882166f63ba0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyoutBody, EuiCallOut, EuiButton } from '@elastic/eui'; + +import { + InvalidDocumentsSummary, + ValidDocumentsSummary, + SchemaFieldsSummary, +} from './summary_sections'; +import { Summary, FlyoutHeader, FlyoutBody, FlyoutFooter } from './summary'; + +describe('Summary', () => { + const values = { + summary: { + invalidDocuments: { + total: 0, + }, + }, + }; + const actions = { + setCreationStep: jest.fn(), + closeDocumentCreation: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(FlyoutHeader)).toHaveLength(1); + expect(wrapper.find(FlyoutBody)).toHaveLength(1); + expect(wrapper.find(FlyoutFooter)).toHaveLength(1); + }); + + describe('FlyoutHeader', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find('h2').text()).toEqual('Indexing summary'); + }); + }); + + describe('FlyoutBody', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(InvalidDocumentsSummary)).toHaveLength(1); + expect(wrapper.find(ValidDocumentsSummary)).toHaveLength(1); + expect(wrapper.find(SchemaFieldsSummary)).toHaveLength(1); + }); + + it('shows an error callout as a flyout banner when the upload contained invalid document(s)', () => { + setMockValues({ summary: { invalidDocuments: { total: 1 } } }); + const wrapper = shallow(); + const banner = wrapper.find(EuiFlyoutBody).prop('banner') as any; + + expect(banner.type).toEqual(EuiCallOut); + expect(banner.props.color).toEqual('danger'); + expect(banner.props.iconType).toEqual('alert'); + expect(banner.props.title).toEqual( + 'Something went wrong. Please address the errors and try again.' + ); + }); + }); + + describe('FlyoutFooter', () => { + it('closes the flyout', () => { + const wrapper = shallow(); + + wrapper.find(EuiButton).simulate('click'); + expect(actions.closeDocumentCreation).toHaveBeenCalled(); + }); + + it('shows a "Fix errors" button when the upload contained invalid document(s)', () => { + setMockValues({ summary: { invalidDocuments: { total: 5 } } }); + const wrapper = shallow(); + + wrapper.find(EuiButton).last().simulate('click'); + expect(actions.setCreationStep).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx new file mode 100644 index 0000000000000..7c7b2c805a710 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.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 React from 'react'; +import { useValues, useActions } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiCallOut, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; + +import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CLOSE_BUTTON, DOCUMENT_CREATION_ERRORS } from '../constants'; +import { DocumentCreationStep } from '../types'; +import { DocumentCreationLogic } from '../'; + +import { + InvalidDocumentsSummary, + ValidDocumentsSummary, + SchemaFieldsSummary, +} from './summary_sections'; + +export const Summary: React.FC = () => { + return ( + <> + + + + + ); +}; + +export const FlyoutHeader: React.FC = () => { + return ( + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.showSummary.title', { + defaultMessage: 'Indexing summary', + })} +

+
+
+ ); +}; + +export const FlyoutBody: React.FC = () => { + const { summary } = useValues(DocumentCreationLogic); + const hasInvalidDocuments = summary.invalidDocuments.total > 0; + const invalidDocumentsBanner = ( + + ); + + return ( + + + + + + ); +}; + +export const FlyoutFooter: React.FC = () => { + const { setCreationStep, closeDocumentCreation } = useActions(DocumentCreationLogic); + const { summary } = useValues(DocumentCreationLogic); + const hasInvalidDocuments = summary.invalidDocuments.total > 0; + + return ( + + + + {FLYOUT_CLOSE_BUTTON} + + {hasInvalidDocuments && ( + + setCreationStep(DocumentCreationStep.AddDocuments)}> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.showSummary.fixErrors', + { defaultMessage: 'Fix errors' } + )} + + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx new file mode 100644 index 0000000000000..790b0b7197383 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.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 from 'react'; +import { shallow } from 'enzyme'; +import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; + +import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; + +describe('ExampleDocumentJson', () => { + const exampleDocument = { hello: 'world' }; + const expectedJson = `{ + "hello": "world" +}`; + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual(expectedJson); + expect(wrapper.find(EuiCallOut)).toHaveLength(0); + }); + + it('renders invalid documents with error callouts', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('h3').text()).toEqual('This document was not indexed!'); + expect(wrapper.find(EuiCallOut)).toHaveLength(2); + expect(wrapper.find(EuiCallOut).first().prop('title')).toEqual('Bad JSON error'); + expect(wrapper.find(EuiCallOut).last().prop('title')).toEqual('Schema error'); + }); +}); + +describe('MoreDocumentsText', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find('p').text()).toEqual('and 100 other documents.'); + + wrapper.setProps({ documents: 1 }); + expect(wrapper.find('p').text()).toEqual('and 1 other document.'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx new file mode 100644 index 0000000000000..338020d26dec0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiCodeBlock, EuiCallOut, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; + +interface ExampleDocumentJsonProps { + document: object; + errors?: string[]; +} +export const ExampleDocumentJson: React.FC = ({ document, errors }) => { + return ( + <> + {errors && ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.showSummary.documentNotIndexed', + { defaultMessage: 'This document was not indexed!' } + )} +

+
+ + {errors.map((errorMessage, index) => ( + + + + + ))} + + )} + + {JSON.stringify(document, null, 2)} + + + + ); +}; + +interface MoreDocumentsTextProps { + documents: number; +} +export const MoreDocumentsText: React.FC = ({ documents }) => { + return ( + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.showSummary.otherDocuments', + { + defaultMessage: + 'and {documents, number} other {documents, plural, one {document} other {documents}}.', + values: { documents }, + } + )} +

+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.scss new file mode 100644 index 0000000000000..029fcdd25554c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.scss @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +.documentCreationSummarySection { + padding: $euiSize $euiSizeM; + color: $euiTextSubduedColor; + border-top: $euiBorderThin; + border-bottom: $euiBorderThin; + + & + & { + border-top: 0; + } + + &__title { + display: flex; + align-items: center; + height: $euiSizeL; + + .euiIcon { + margin-right: $euiSizeS; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx new file mode 100644 index 0000000000000..0af2327c6bbac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx @@ -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 React, { ReactElement } from 'react'; +import { shallow } from 'enzyme'; +import { EuiAccordion, EuiIcon } from '@elastic/eui'; + +import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; + +describe('SummarySectionAccordion', () => { + const props = { + id: 'some-id', + status: 'success' as 'success' | 'error' | 'info', + title: 'Some title', + }; + + it('renders', () => { + const wrapper = shallow( + Hello World + ); + + expect(wrapper.type()).toEqual(EuiAccordion); + expect(wrapper.hasClass('documentCreationSummarySection')).toBe(true); + expect(wrapper.find(EuiAccordion).prop('children')).toEqual('Hello World'); + }); + + it('renders a title', () => { + const wrapper = shallow(); + const buttonContent = shallow(wrapper.find(EuiAccordion).prop('buttonContent') as ReactElement); + + expect(buttonContent.find('.documentCreationSummarySection__title').text()).toEqual( + 'Hello World' + ); + }); + + it('renders icons based on the status prop', () => { + const wrapper = shallow(); + const getIcon = () => { + const buttonContent = shallow( + wrapper.find(EuiAccordion).prop('buttonContent') as ReactElement + ); + return buttonContent.find(EuiIcon); + }; + + wrapper.setProps({ status: 'error' }); + expect(getIcon().prop('type')).toEqual('crossInACircleFilled'); + expect(getIcon().prop('color')).toEqual('danger'); + + wrapper.setProps({ status: 'success' }); + expect(getIcon().prop('type')).toEqual('checkInCircleFilled'); + expect(getIcon().prop('color')).toEqual('success'); + + wrapper.setProps({ status: 'info' }); + expect(getIcon().prop('type')).toEqual('iInCircle'); + expect(getIcon().prop('color')).toEqual('default'); + }); +}); + +describe('SummarySectionEmpty', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('documentCreationSummarySection')).toBe(true); + expect(wrapper.find('.documentCreationSummarySection__title').text()).toEqual( + 'No new documents' + ); + expect(wrapper.find(EuiIcon).prop('type')).toEqual('iInCircle'); + expect(wrapper.find(EuiIcon).prop('color')).toEqual('default'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.tsx new file mode 100644 index 0000000000000..d50779e7ff003 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiAccordion, EuiIcon } from '@elastic/eui'; + +import './summary_section.scss'; + +const ICON_PROPS = { + error: { type: 'crossInACircleFilled', color: 'danger' }, + success: { type: 'checkInCircleFilled', color: 'success' }, + info: { type: 'iInCircle', color: 'default' }, +}; + +interface SummarySectionAccordionProps { + id: string; + status: 'success' | 'error' | 'info'; + title: string; +} +export const SummarySectionAccordion: React.FC = ({ + id, + status, + title, + children, +}) => { + return ( + + + {title} +
+ } + > + {children} + + ); +}; + +interface SummarySectionEmptyProps { + title: string; +} +export const SummarySectionEmpty: React.FC = ({ title }) => { + return ( +
+
+ + {title} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx new file mode 100644 index 0000000000000..86cea8ef23587 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx @@ -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 { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiBadge } from '@elastic/eui'; +import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; +import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; + +import { + InvalidDocumentsSummary, + ValidDocumentsSummary, + SchemaFieldsSummary, +} from './summary_sections'; + +describe('InvalidDocumentsSummary', () => { + const mockDocument = { hello: 'world' }; + const mockExample = { document: mockDocument, errors: ['bad schema'] }; + + it('renders', () => { + setMockValues({ + summary: { + invalidDocuments: { + total: 1, + examples: [mockExample], + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionAccordion).prop('title')).toEqual( + '1 document with errors...' + ); + expect(wrapper.find(ExampleDocumentJson)).toHaveLength(1); + expect(wrapper.find(MoreDocumentsText)).toHaveLength(0); + }); + + it('renders with MoreDocumentsText if more than 5 documents exist', () => { + setMockValues({ + summary: { + invalidDocuments: { + total: 100, + examples: [mockExample, mockExample, mockExample, mockExample, mockExample], + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionAccordion).prop('title')).toEqual( + '100 documents with errors...' + ); + expect(wrapper.find(ExampleDocumentJson)).toHaveLength(5); + expect(wrapper.find(MoreDocumentsText)).toHaveLength(1); + expect(wrapper.find(MoreDocumentsText).prop('documents')).toEqual(95); + }); + + it('does not render if there are no invalid documents', () => { + setMockValues({ + summary: { + invalidDocuments: { + total: 0, + examples: [], + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); + +describe('ValidDocumentsSummary', () => { + const mockDocument = { hello: 'world' }; + + it('renders', () => { + setMockValues({ + summary: { + validDocuments: { + total: 1, + examples: [mockDocument], + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionAccordion).prop('title')).toEqual('Added 1 document.'); + expect(wrapper.find(ExampleDocumentJson)).toHaveLength(1); + expect(wrapper.find(MoreDocumentsText)).toHaveLength(0); + }); + + it('renders with MoreDocumentsText if more than 5 documents exist', () => { + setMockValues({ + summary: { + validDocuments: { + total: 7, + examples: [mockDocument, mockDocument, mockDocument, mockDocument, mockDocument], + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionAccordion).prop('title')).toEqual('Added 7 documents.'); + expect(wrapper.find(ExampleDocumentJson)).toHaveLength(5); + expect(wrapper.find(MoreDocumentsText)).toHaveLength(1); + expect(wrapper.find(MoreDocumentsText).prop('documents')).toEqual(2); + }); + + it('renders SummarySectionEmpty if there are no valid documents', () => { + setMockValues({ + summary: { + validDocuments: { + total: 0, + examples: [], + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionEmpty).prop('title')).toEqual('No new documents.'); + }); +}); + +describe('SchemaFieldsSummary', () => { + it('renders', () => { + setMockValues({ + summary: { + newSchemaFields: ['test'], + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionAccordion).prop('title')).toEqual( + "Added 1 field to the Engine's schema." + ); + expect(wrapper.find(EuiBadge)).toHaveLength(1); + }); + + it('renders multiple new schema fields', () => { + setMockValues({ + summary: { + newSchemaFields: ['foo', 'bar', 'baz', 'qux', 'quux', 'quuz'], + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionAccordion).prop('title')).toEqual( + "Added 6 fields to the Engine's schema." + ); + expect(wrapper.find(EuiBadge)).toHaveLength(6); + }); + + it('renders SummarySectionEmpty if there are no new schema fields', () => { + setMockValues({ + summary: { + newSchemaFields: [], + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SummarySectionEmpty).prop('title')).toEqual('No new schema fields.'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx new file mode 100644 index 0000000000000..2a13622dfbc8e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; + +import { DocumentCreationLogic } from '../'; + +import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; +import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; + +export const InvalidDocumentsSummary: React.FC = () => { + const { + summary: { invalidDocuments }, + } = useValues(DocumentCreationLogic); + + const hasInvalidDocuments = invalidDocuments.total > 0; + const unshownInvalidDocuments = invalidDocuments.total - invalidDocuments.examples.length; + + return hasInvalidDocuments ? ( + + {invalidDocuments.examples.map(({ document, errors }, index) => ( + + ))} + {unshownInvalidDocuments > 0 && } + + ) : null; +}; + +export const ValidDocumentsSummary: React.FC = () => { + const { + summary: { validDocuments }, + } = useValues(DocumentCreationLogic); + + const hasValidDocuments = validDocuments.total > 0; + const unshownValidDocuments = validDocuments.total - validDocuments.examples.length; + + return hasValidDocuments ? ( + + {validDocuments.examples.map((document, index) => ( + + ))} + {unshownValidDocuments > 0 && } + + ) : ( + + ); +}; + +export const SchemaFieldsSummary: React.FC = () => { + const { + summary: { newSchemaFields }, + } = useValues(DocumentCreationLogic); + + return newSchemaFields.length ? ( + + + {newSchemaFields.map((schemaField: string) => ( + + {schemaField} + + ))} + + + ) : ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx index f2799cde41e97..cc9a671e41e5d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx @@ -16,6 +16,7 @@ import { PasteJsonText, UploadJsonFile, } from './creation_mode_components'; +import { Summary } from './creation_response_components'; import { DocumentCreationStep } from './types'; import { DocumentCreationFlyout, FlyoutContent } from './document_creation_flyout'; @@ -82,28 +83,11 @@ describe('DocumentCreationFlyout', () => { }); }); - describe('creation steps', () => { - it('renders an error page', () => { - setMockValues({ ...values, creationStep: DocumentCreationStep.ShowError }); - const wrapper = shallow(); - - expect(wrapper.text()).toBe('DocumentCreationError'); // TODO: actual component - }); - - it('renders an error summary', () => { - setMockValues({ ...values, creationStep: DocumentCreationStep.ShowErrorSummary }); - const wrapper = shallow(); - - expect(wrapper.text()).toBe('DocumentCreationSummary'); // TODO: actual component - }); - - it('renders a success summary', () => { - setMockValues({ ...values, creationStep: DocumentCreationStep.ShowSuccessSummary }); - const wrapper = shallow(); + it('renders a summary', () => { + setMockValues({ ...values, creationStep: DocumentCreationStep.ShowSummary }); + const wrapper = shallow(); - // TODO: Figure out if the error and success summary should remain the same vs different components - expect(wrapper.text()).toBe('DocumentCreationSummary'); // TODO: actual component - }); + expect(wrapper.find(Summary)).toHaveLength(1); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx index ca52d14befb38..2dd00f0ded17d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx @@ -19,6 +19,7 @@ import { PasteJsonText, UploadJsonFile, } from './creation_mode_components'; +import { Summary } from './creation_response_components'; export const DocumentCreationFlyout: React.FC = () => { const { closeDocumentCreation } = useActions(DocumentCreationLogic); @@ -48,11 +49,7 @@ export const FlyoutContent: React.FC = () => { case 'file': return ; } - case DocumentCreationStep.ShowError: - return <>DocumentCreationError; - case DocumentCreationStep.ShowErrorSummary: - return <>DocumentCreationSummary; - case DocumentCreationStep.ShowSuccessSummary: - return <>DocumentCreationSummary; + case DocumentCreationStep.ShowSummary: + return ; } }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index 1145d7853cb1a..c2a0d29cc1f40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -4,9 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; + import dedent from 'dedent'; +jest.mock('./utils', () => ({ + readUploadedFileAsText: jest.fn(), +})); +import { readUploadedFileAsText } from './utils'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: { http: { post: jest.fn() } } }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'test-engine' } }, +})); + import { DOCUMENTS_API_JSON_EXAMPLE } from './constants'; import { DocumentCreationStep } from './types'; import { DocumentCreationLogic } from './'; @@ -18,13 +33,14 @@ describe('DocumentCreationLogic', () => { creationStep: DocumentCreationStep.AddDocuments, textInput: dedent(DOCUMENTS_API_JSON_EXAMPLE), fileInput: null, + isUploading: false, + warnings: [], + errors: [], + summary: {}, }; const mockFile = new File(['mockFile'], 'mockFile.json'); - const mount = () => { - resetContext({}); - DocumentCreationLogic.mount(); - }; + const { mount } = new LogicMounter(DocumentCreationLogic); beforeEach(() => { jest.clearAllMocks(); @@ -120,17 +136,39 @@ describe('DocumentCreationLogic', () => { }); }); }); + + describe('errors & warnings', () => { + it('should be cleared', () => { + mount({ errors: ['error'], warnings: ['warnings'] }); + DocumentCreationLogic.actions.closeDocumentCreation(); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + errors: [], + warnings: [], + }); + }); + }); + + describe('textInput & fileInput', () => { + it('should be reset to default values', () => { + mount({ textInput: 'test', fileInput: mockFile }); + DocumentCreationLogic.actions.closeDocumentCreation(); + + expect(DocumentCreationLogic.values).toEqual(DEFAULT_VALUES); + }); + }); }); describe('setCreationStep', () => { describe('creationStep', () => { it('should be set to the provided value', () => { mount(); - DocumentCreationLogic.actions.setCreationStep(DocumentCreationStep.ShowSuccessSummary); + DocumentCreationLogic.actions.setCreationStep(DocumentCreationStep.ShowSummary); expect(DocumentCreationLogic.values).toEqual({ ...DEFAULT_VALUES, - creationStep: 3, + creationStep: 2, }); }); }); @@ -163,5 +201,393 @@ describe('DocumentCreationLogic', () => { }); }); }); + + describe('setWarnings', () => { + describe('warnings', () => { + it('should be set to the provided value', () => { + mount(); + DocumentCreationLogic.actions.setWarnings(['warning!']); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + warnings: ['warning!'], + }); + }); + }); + }); + + describe('setErrors', () => { + describe('errors', () => { + beforeAll(() => { + mount(); + }); + + it('should be set to the provided value', () => { + DocumentCreationLogic.actions.setErrors(['error 1', 'error 2']); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + errors: ['error 1', 'error 2'], + }); + }); + + it('should gracefully array wrap single errors', () => { + DocumentCreationLogic.actions.setErrors('error'); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + errors: ['error'], + }); + }); + }); + + describe('isUploading', () => { + it('resets isUploading to false', () => { + mount({ isUploading: true }); + DocumentCreationLogic.actions.setErrors(['error']); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + errors: ['error'], + isUploading: false, + }); + }); + }); + }); + + describe('setSummary', () => { + const mockSummary = { + errors: [], + validDocuments: { + total: 1, + examples: [{ foo: 'bar' }], + }, + invalidDocuments: { + total: 0, + examples: [], + }, + newSchemaFields: ['foo'], + }; + + describe('summary', () => { + it('should be set to the provided value', () => { + mount(); + DocumentCreationLogic.actions.setSummary(mockSummary); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + summary: mockSummary, + }); + }); + }); + + describe('isUploading', () => { + it('resets isUploading to false', () => { + mount({ isUploading: true }); + DocumentCreationLogic.actions.setSummary(mockSummary); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + summary: mockSummary, + isUploading: false, + }); + }); + }); + }); + + describe('onSubmitFile', () => { + describe('with a valid file', () => { + beforeAll(() => { + mount({ fileInput: mockFile }); + jest.spyOn(DocumentCreationLogic.actions, 'onSubmitJson').mockImplementation(); + }); + + it('should read the text in the file and submit it as JSON', async () => { + (readUploadedFileAsText as jest.Mock).mockReturnValue(Promise.resolve('some mock text')); + await DocumentCreationLogic.actions.onSubmitFile(); + + expect(DocumentCreationLogic.values.textInput).toEqual('some mock text'); + expect(DocumentCreationLogic.actions.onSubmitJson).toHaveBeenCalled(); + }); + + it('should set isUploading to true', () => { + DocumentCreationLogic.actions.onSubmitFile(); + + expect(DocumentCreationLogic.values.isUploading).toEqual(true); + }); + }); + + describe('with an invalid file', () => { + beforeAll(() => { + mount({ fileInput: mockFile }); + jest.spyOn(DocumentCreationLogic.actions, 'onSubmitJson'); + jest.spyOn(DocumentCreationLogic.actions, 'setErrors'); + }); + + it('should return an error', async () => { + (readUploadedFileAsText as jest.Mock).mockReturnValue(Promise.reject()); + await DocumentCreationLogic.actions.onSubmitFile(); + + expect(DocumentCreationLogic.actions.onSubmitJson).not.toHaveBeenCalled(); + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith([ + 'Problem parsing file.', + ]); + }); + }); + + describe('without a file', () => { + beforeAll(() => { + mount(); + jest.spyOn(DocumentCreationLogic.actions, 'onSubmitJson'); + jest.spyOn(DocumentCreationLogic.actions, 'setErrors'); + }); + + it('should return an error', () => { + DocumentCreationLogic.actions.onSubmitFile(); + + expect(DocumentCreationLogic.actions.onSubmitJson).not.toHaveBeenCalled(); + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith(['No file found.']); + }); + }); + }); + + describe('onSubmitJson', () => { + describe('with large JSON files', () => { + beforeAll(() => { + mount(); + jest.spyOn(DocumentCreationLogic.actions, 'uploadDocuments').mockImplementation(); + jest.spyOn(DocumentCreationLogic.actions, 'setWarnings'); + }); + + it('should set a warning', () => { + jest.spyOn(global.Buffer, 'byteLength').mockImplementation(() => 55000000); // 55MB + DocumentCreationLogic.actions.onSubmitJson(); + + expect(DocumentCreationLogic.actions.setWarnings).toHaveBeenCalledWith([ + expect.stringContaining("You're uploading an extremely large file"), + ]); + + jest.restoreAllMocks(); + }); + }); + + describe('with invalid JSON', () => { + beforeAll(() => { + mount(); + jest.spyOn(DocumentCreationLogic.actions, 'uploadDocuments').mockImplementation(); + jest.spyOn(DocumentCreationLogic.actions, 'setErrors'); + }); + + it('should return malformed JSON errors', () => { + DocumentCreationLogic.actions.setTextInput('invalid JSON'); + DocumentCreationLogic.actions.onSubmitJson(); + + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith([ + 'Unexpected token i in JSON at position 0', + ]); + expect(DocumentCreationLogic.actions.uploadDocuments).not.toHaveBeenCalled(); + }); + + it('should error on non-array/object JSON', () => { + DocumentCreationLogic.actions.setTextInput('null'); + DocumentCreationLogic.actions.onSubmitJson(); + + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith([ + 'Document contents must be a valid JSON array or object.', + ]); + expect(DocumentCreationLogic.actions.uploadDocuments).not.toHaveBeenCalled(); + }); + }); + + describe('with valid JSON', () => { + beforeAll(() => { + mount(); + jest.spyOn(DocumentCreationLogic.actions, 'uploadDocuments').mockImplementation(); + jest.spyOn(DocumentCreationLogic.actions, 'setErrors'); + }); + + it('should accept an array of JSON objs', () => { + const mockJson = [{ foo: 'bar' }, { bar: 'baz' }]; + DocumentCreationLogic.actions.setTextInput('[{"foo":"bar"},{"bar":"baz"}]'); + DocumentCreationLogic.actions.onSubmitJson(); + + expect(DocumentCreationLogic.actions.uploadDocuments).toHaveBeenCalledWith({ + documents: mockJson, + }); + expect(DocumentCreationLogic.actions.setErrors).not.toHaveBeenCalled(); + }); + + it('should accept a single JSON obj', () => { + const mockJson = { foo: 'bar' }; + DocumentCreationLogic.actions.setTextInput('{"foo":"bar"}'); + DocumentCreationLogic.actions.onSubmitJson(); + + expect(DocumentCreationLogic.actions.uploadDocuments).toHaveBeenCalledWith({ + documents: [mockJson], + }); + expect(DocumentCreationLogic.actions.setErrors).not.toHaveBeenCalled(); + }); + }); + }); + + describe('uploadDocuments', () => { + describe('valid uploads', () => { + const mockValidDocuments = [{ foo: 'bar', bar: 'baz', qux: 'quux' }]; + const mockValidResponse = { + errors: [], + validDocuments: { total: 3, examples: mockValidDocuments }, + invalidDocuments: { total: 0, examples: [] }, + newSchemaFields: ['foo', 'bar', 'qux'], + }; + + beforeAll(() => { + mount(); + jest.spyOn(DocumentCreationLogic.actions, 'setSummary'); + jest.spyOn(DocumentCreationLogic.actions, 'setCreationStep'); + }); + + it('should set and show summary from the returned response', async () => { + const { http } = HttpLogic.values; + const promise = (http.post as jest.Mock).mockReturnValueOnce( + Promise.resolve(mockValidResponse) + ); + + await DocumentCreationLogic.actions.uploadDocuments({ documents: mockValidDocuments }); + await promise; + + expect(DocumentCreationLogic.actions.setSummary).toHaveBeenCalledWith(mockValidResponse); + expect(DocumentCreationLogic.actions.setCreationStep).toHaveBeenCalledWith( + DocumentCreationStep.ShowSummary + ); + }); + }); + + describe('invalid uploads', () => { + beforeAll(() => { + mount(); + jest.spyOn(DocumentCreationLogic.actions, 'setErrors'); + }); + + it('handles API errors', async () => { + const { http } = HttpLogic.values; + const promise = (http.post as jest.Mock).mockReturnValueOnce( + Promise.reject({ + body: { + statusCode: 400, + error: 'Bad Request', + message: 'Invalid request payload JSON format', + }, + }) + ); + + await DocumentCreationLogic.actions.uploadDocuments({ documents: [{}] }); + await promise; + + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith( + '[400 Bad Request] Invalid request payload JSON format' + ); + }); + + it('handles client-side errors', async () => { + const { http } = HttpLogic.values; + const promise = (http.post as jest.Mock).mockReturnValueOnce(new Error()); + + await DocumentCreationLogic.actions.uploadDocuments({ documents: [{}] }); + await promise; + + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith( + "Cannot read property 'total' of undefined" + ); + }); + + // NOTE: I can't seem to reproduce this in a production setting. + it('handles errors returned from the API', async () => { + const { http } = HttpLogic.values; + const promise = (http.post as jest.Mock).mockReturnValueOnce( + Promise.resolve({ + errors: ['JSON cannot be empty'], + }) + ); + + await DocumentCreationLogic.actions.uploadDocuments({ documents: [{}] }); + await promise; + + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith([ + 'JSON cannot be empty', + ]); + }); + }); + + describe('chunks large uploads', () => { + // Using an array of #s for speed, it doesn't really matter what the contents of the documents are for this test + const largeDocumentsArray = ([...Array(200).keys()] as unknown) as object[]; + + const mockFirstResponse = { + validDocuments: { total: 99, examples: largeDocumentsArray.slice(0, 98) }, + invalidDocuments: { + total: 1, + examples: [{ document: largeDocumentsArray[99], error: ['some error'] }], + }, + newSchemaFields: ['foo', 'bar'], + }; + const mockSecondResponse = { + validDocuments: { total: 99, examples: largeDocumentsArray.slice(1, 99) }, + invalidDocuments: { + total: 1, + examples: [{ document: largeDocumentsArray[0], error: ['another error'] }], + }, + newSchemaFields: ['bar', 'baz'], + }; + + beforeAll(() => { + mount(); + jest.spyOn(DocumentCreationLogic.actions, 'setSummary'); + jest.spyOn(DocumentCreationLogic.actions, 'setErrors'); + }); + + it('should correctly merge multiple API calls into a single summary obj', async () => { + const { http } = HttpLogic.values; + const promise = (http.post as jest.Mock) + .mockReturnValueOnce(mockFirstResponse) + .mockReturnValueOnce(mockSecondResponse); + + await DocumentCreationLogic.actions.uploadDocuments({ documents: largeDocumentsArray }); + await promise; + + expect(http.post).toHaveBeenCalledTimes(2); + expect(DocumentCreationLogic.actions.setSummary).toHaveBeenCalledWith({ + errors: [], + validDocuments: { + total: 198, + examples: largeDocumentsArray.slice(0, 5), + }, + invalidDocuments: { + total: 2, + examples: [ + { document: largeDocumentsArray[99], error: ['some error'] }, + { document: largeDocumentsArray[0], error: ['another error'] }, + ], + }, + newSchemaFields: ['foo', 'bar', 'baz'], + }); + }); + + it('should correctly merge response errors', async () => { + const { http } = HttpLogic.values; + const promise = (http.post as jest.Mock) + .mockReturnValueOnce({ ...mockFirstResponse, errors: ['JSON cannot be empty'] }) + .mockReturnValueOnce({ ...mockSecondResponse, errors: ['Too large to render'] }); + + await DocumentCreationLogic.actions.uploadDocuments({ documents: largeDocumentsArray }); + await promise; + + expect(http.post).toHaveBeenCalledTimes(2); + expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith([ + 'JSON cannot be empty', + 'Too large to render', + ]); + }); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts index 5b85e7f2ab54e..119baed74f684 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts @@ -6,9 +6,18 @@ import { kea, MakeLogicType } from 'kea'; import dedent from 'dedent'; +import { isPlainObject, chunk, uniq } from 'lodash'; -import { DOCUMENTS_API_JSON_EXAMPLE } from './constants'; -import { DocumentCreationMode, DocumentCreationStep } from './types'; +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; + +import { + DOCUMENTS_API_JSON_EXAMPLE, + DOCUMENT_CREATION_ERRORS, + DOCUMENT_CREATION_WARNINGS, +} from './constants'; +import { DocumentCreationMode, DocumentCreationStep, DocumentCreationSummary } from './types'; +import { readUploadedFileAsText } from './utils'; interface DocumentCreationValues { isDocumentCreationOpen: boolean; @@ -16,6 +25,10 @@ interface DocumentCreationValues { creationStep: DocumentCreationStep; textInput: string; fileInput: File | null; + isUploading: boolean; + warnings: string[]; + errors: string[]; + summary: DocumentCreationSummary; } interface DocumentCreationActions { @@ -25,6 +38,12 @@ interface DocumentCreationActions { setCreationStep(creationStep: DocumentCreationStep): { creationStep: DocumentCreationStep }; setTextInput(textInput: string): { textInput: string }; setFileInput(fileInput: File | null): { fileInput: File | null }; + setWarnings(warnings: string[]): { warnings: string[] }; + setErrors(errors: string[] | string): { errors: string[] }; + setSummary(summary: DocumentCreationSummary): { summary: DocumentCreationSummary }; + onSubmitFile(): void; + onSubmitJson(): void; + uploadDocuments(args: { documents: object[] }): { documents: object[] }; } export const DocumentCreationLogic = kea< @@ -38,6 +57,12 @@ export const DocumentCreationLogic = kea< setCreationStep: (creationStep) => ({ creationStep }), setTextInput: (textInput) => ({ textInput }), setFileInput: (fileInput) => ({ fileInput }), + setWarnings: (warnings) => ({ warnings }), + setErrors: (errors) => ({ errors }), + setSummary: (summary) => ({ summary }), + onSubmitJson: () => null, + onSubmitFile: () => null, + uploadDocuments: ({ documents }) => ({ documents }), }), reducers: () => ({ isDocumentCreationOpen: [ @@ -66,13 +91,134 @@ export const DocumentCreationLogic = kea< dedent(DOCUMENTS_API_JSON_EXAMPLE), { setTextInput: (_, { textInput }) => textInput, + closeDocumentCreation: () => dedent(DOCUMENTS_API_JSON_EXAMPLE), }, ], fileInput: [ null, { setFileInput: (_, { fileInput }) => fileInput, + closeDocumentCreation: () => null, + }, + ], + isUploading: [ + false, + { + onSubmitFile: () => true, + onSubmitJson: () => true, + setErrors: () => false, + setSummary: () => false, + }, + ], + warnings: [ + [], + { + onSubmitJson: () => [], + setWarnings: (_, { warnings }) => warnings, + closeDocumentCreation: () => [], + }, + ], + errors: [ + [], + { + onSubmitJson: () => [], + setErrors: (_, { errors }) => (Array.isArray(errors) ? errors : [errors]), + closeDocumentCreation: () => [], }, ], + summary: [ + {} as DocumentCreationSummary, + { + setSummary: (_, { summary }) => summary, + }, + ], + }), + listeners: ({ values, actions }) => ({ + onSubmitFile: async () => { + const { fileInput } = values; + + if (!fileInput) { + return actions.setErrors([DOCUMENT_CREATION_ERRORS.NO_FILE]); + } + try { + const textInput = await readUploadedFileAsText(fileInput); + actions.setTextInput(textInput); + actions.onSubmitJson(); + } catch { + actions.setErrors([DOCUMENT_CREATION_ERRORS.NO_VALID_FILE]); + } + }, + onSubmitJson: () => { + const { textInput } = values; + + const MAX_UPLOAD_BYTES = 50 * 1000000; // 50 MB + if (Buffer.byteLength(textInput) > MAX_UPLOAD_BYTES) { + actions.setWarnings([DOCUMENT_CREATION_WARNINGS.LARGE_FILE]); + } + + let documents; + try { + documents = JSON.parse(textInput); + } catch (error) { + return actions.setErrors([error.message]); + } + + if (Array.isArray(documents)) { + actions.uploadDocuments({ documents }); + } else if (isPlainObject(documents)) { + actions.uploadDocuments({ documents: [documents] }); + } else { + actions.setErrors([DOCUMENT_CREATION_ERRORS.NOT_VALID]); + } + }, + uploadDocuments: async ({ documents }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const CHUNK_SIZE = 100; + const MAX_EXAMPLES = 5; + + const promises = chunk(documents, CHUNK_SIZE).map((documentsChunk) => { + const body = JSON.stringify({ documents: documentsChunk }); + return http.post(`/api/app_search/engines/${engineName}/documents`, { body }); + }); + + try { + const responses = await Promise.all(promises); + const summary: DocumentCreationSummary = { + errors: [], + validDocuments: { total: 0, examples: [] }, + invalidDocuments: { total: 0, examples: [] }, + newSchemaFields: [], + }; + responses.forEach((response) => { + if (response.errors?.length > 0) { + summary.errors = uniq([...summary.errors, ...response.errors]); + return; + } + summary.validDocuments.total += response.validDocuments.total; + summary.invalidDocuments.total += response.invalidDocuments.total; + summary.validDocuments.examples = [ + ...summary.validDocuments.examples, + ...response.validDocuments.examples, + ].slice(0, MAX_EXAMPLES); + summary.invalidDocuments.examples = [ + ...summary.invalidDocuments.examples, + ...response.invalidDocuments.examples, + ].slice(0, MAX_EXAMPLES); + summary.newSchemaFields = uniq([...summary.newSchemaFields, ...response.newSchemaFields]); + }); + + if (summary.errors.length > 0) { + actions.setErrors(summary.errors); + } else { + actions.setSummary(summary); + actions.setCreationStep(DocumentCreationStep.ShowSummary); + } + } catch ({ body, message }) { + const errors = body ? `[${body.statusCode} ${body.error}] ${body.message}` : message; + actions.setErrors(errors); + } + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/types.ts index d29bff162c197..ba641326f76b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/types.ts @@ -9,7 +9,21 @@ export type DocumentCreationMode = 'text' | 'file' | 'api'; export enum DocumentCreationStep { ShowCreationModes, AddDocuments, - ShowErrorSummary, - ShowSuccessSummary, - ShowError, + ShowSummary, +} + +export interface DocumentCreationSummary { + errors: string[]; + validDocuments: { + total: number; + examples: object[]; + }; + invalidDocuments: { + total: number; + examples: Array<{ + document: object; + errors: string[]; + }>; + }; + newSchemaFields: string[]; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/utils.test.ts new file mode 100644 index 0000000000000..0df98c8d3030e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/utils.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { readUploadedFileAsText } from './utils'; + +describe('readUploadedFileAsText', () => { + it('reads a file as text', async () => { + const file = new File(['a mock file'], 'mockFile.json'); + const text = await readUploadedFileAsText(file); + expect(text).toEqual('a mock file'); + }); + + it('throws an error if the file cannot be read', async () => { + const badFile = ('causes an error' as unknown) as File; + await expect(readUploadedFileAsText(badFile)).rejects.toThrow(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/utils.ts new file mode 100644 index 0000000000000..d2b207c51d22a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/utils.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. + */ + +export const readUploadedFileAsText = (fileInput: File): Promise => { + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = () => { + resolve(reader.result as string); + }; + try { + reader.readAsText(fileInput); + } catch { + reader.abort(); + reject(new Error()); + } + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index d9a3de7c078cc..fe735f70247c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -36,24 +36,7 @@ describe('DocumentDetailLogic', () => { fields: [], }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - document_detail_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - DocumentDetailLogic.mount(); - }; + const { mount } = new LogicMounter(DocumentDetailLogic); beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts index 236172f0f7bdf..2863a39535ef4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { DocumentsLogic } from './documents_logic'; @@ -13,24 +13,7 @@ describe('DocumentsLogic', () => { isDocumentCreationOpen: false, }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - documents_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - DocumentsLogic.mount(); - }; + const { mount } = new LogicMounter(DocumentsLogic); describe('actions', () => { describe('openDocumentCreation', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts index b67e444939c0e..113259b7bd899 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { DOCUMENTS_TITLE } from './constants'; export { DocumentDetailLogic } from './document_detail_logic'; export { DocumentsLogic } from './documents_logic'; export { Documents } from './documents'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index 533adbaf5bab9..78e1fa9e7f3a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -16,19 +16,16 @@ export const buildSearchUIConfig = (apiConnector: object, schema: Schema) => { sortField: 'id', }, searchQuery: { - result_fields: Object.keys(schema || {}).reduce( - (acc: { [key: string]: object }, key: string) => { - acc[key] = { - snippet: { - size: 300, - fallback: true, - }, - raw: {}, - }; - return acc; - }, - {} - ), + result_fields: Object.keys(schema).reduce((acc: { [key: string]: object }, key: string) => { + acc[key] = { + snippet: { + size: 300, + fallback: true, + }, + raw: {}, + }; + return acc; + }, {}), }, }; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index a46ec560a13e0..8fc1ed5a0a4b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -46,30 +46,32 @@ describe('SearchExperienceContent', () => { expect(wrapper.isEmptyRender()).toBe(false); }); - it('passes engineName and schema to the result view', () => { - const props = { - result: { - id: { - raw: '1', - }, - _meta: { - id: '1', - scopedId: '1', - score: 100, - engine: 'my-engine', - }, - foo: { - raw: 'bar', - }, + it('passes result, schema, and isMetaEngine to the result view', () => { + const result = { + id: { + raw: '1', }, - schemaForTypeHighlights: { - title: 'string' as SchemaTypes, + _meta: { + id: '1', + score: 100, + engine: 'my-engine', + }, + foo: { + raw: 'bar', }, }; const wrapper = shallow(); const resultView: any = wrapper.find(Results).prop('resultView'); - expect(resultView(props)).toEqual(); + expect(resultView({ result })).toEqual( + + ); }); it('renders pagination', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx index 55a8377261dd9..b44f3115932a3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx @@ -14,12 +14,12 @@ import { useValues } from 'kea'; import { ResultView } from './views'; import { Pagination } from './pagination'; -import { Props as ResultViewProps } from './views/result_view'; import { useSearchContextState } from './hooks'; import { DocumentCreationButton } from '../document_creation_button'; import { AppLogic } from '../../../app_logic'; import { EngineLogic } from '../../engine'; import { DOCS_PREFIX } from '../../../routes'; +import { Result } from '../../result/types'; export const SearchExperienceContent: React.FC = () => { const { resultSearchTerm, totalResults, wasSearched } = useSearchContextState(); @@ -43,8 +43,14 @@ export const SearchExperienceContent: React.FC = () => { { - return ; + resultView={({ result }: { result: Result }) => { + return ( + + ); }} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx index 91334f312623d..d3a61c12901d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx @@ -22,7 +22,6 @@ describe('ResultView', () => { }, _meta: { id: '1', - scopedId: '1', score: 100, engine: 'my-engine', }, @@ -33,11 +32,14 @@ describe('ResultView', () => { }; it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find(Result).props()).toEqual({ result, shouldLinkToDetailPage: true, schemaForTypeHighlights: schema, + isMetaEngine: true, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx index 543c63b334940..2a17dd6128536 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx @@ -13,15 +13,17 @@ import { Result } from '../../../result/result'; export interface Props { result: ResultType; schemaForTypeHighlights?: Schema; + isMetaEngine: boolean; } -export const ResultView: React.FC = ({ result, schemaForTypeHighlights }) => { +export const ResultView: React.FC = ({ result, schemaForTypeHighlights, isMetaEngine }) => { return (
  • ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts deleted file mode 100644 index 9ce524038075b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -// TODO: It's very likely that we'll move these i18n constants to their respective component -// folders once those are migrated over. This is a temporary way of DRYing them out for now. - -export const ANALYTICS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.title', - { defaultMessage: 'Analytics' } -); -export const DOCUMENTS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.documents.title', - { defaultMessage: 'Documents' } -); -export const SCHEMA_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engine.schema.title', { - defaultMessage: 'Schema', -}); -export const CRAWLER_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.crawler.title', - { defaultMessage: 'Crawler' } -); -export const RELEVANCE_TUNING_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.title', - { defaultMessage: 'Relevance Tuning' } -); -export const SYNONYMS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.synonyms.title', - { defaultMessage: 'Synonyms' } -); -export const CURATIONS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.curations.title', - { defaultMessage: 'Curations' } -); -export const RESULT_SETTINGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.title', - { defaultMessage: 'Result Settings' } -); -export const SEARCH_UI_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.searchUI.title', - { defaultMessage: 'Search UI' } -); -export const API_LOGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.apiLogs.title', - { defaultMessage: 'API Logs' } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index 13db440df739e..094260b6df095 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -46,24 +46,7 @@ describe('EngineLogic', () => { engineNotFound: false, }; - const mount = (values?: object) => { - if (!values) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - engine_logic: { - ...values, - }, - }, - }, - }, - }); - } - EngineLogic.mount(); - }; + const { mount } = new LogicMounter(EngineLogic); beforeEach(() => { jest.clearAllMocks(); @@ -153,6 +136,18 @@ describe('EngineLogic', () => { }); }); + describe('engineName', () => { + it('should be reset to an empty string', () => { + mount({ engineName: 'hello-world' }); + EngineLogic.actions.clearEngine(); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engineName: '', + }); + }); + }); + describe('dataLoading', () => { it('should be set to true', () => { mount({ dataLoading: false }); @@ -164,6 +159,18 @@ describe('EngineLogic', () => { }); }); }); + + describe('engineNotFound', () => { + it('should be set to false', () => { + mount({ engineNotFound: true }); + EngineLogic.actions.clearEngine(); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engineNotFound: false, + }); + }); + }); }); describe('initializeEngine', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index e1ce7cea0fa91..9f3fe721b74de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -64,12 +64,14 @@ export const EngineLogic = kea>({ '', { setEngineName: (_, { engineName }) => engineName, + clearEngine: () => '', }, ], engineNotFound: [ false, { setEngineNotFound: (_, { notFound }) => notFound, + clearEngine: () => false, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 0fed7cd0fc8fc..418ab33457d0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -29,18 +29,16 @@ import { import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { ENGINES_TITLE } from '../engines'; import { OVERVIEW_TITLE } from '../engine_overview'; -import { - ANALYTICS_TITLE, - DOCUMENTS_TITLE, - SCHEMA_TITLE, - CRAWLER_TITLE, - RELEVANCE_TUNING_TITLE, - SYNONYMS_TITLE, - CURATIONS_TITLE, - RESULT_SETTINGS_TITLE, - SEARCH_UI_TITLE, - API_LOGS_TITLE, -} from './constants'; +import { ANALYTICS_TITLE } from '../analytics'; +import { DOCUMENTS_TITLE } from '../documents'; +import { SCHEMA_TITLE } from '../schema'; +import { CRAWLER_TITLE } from '../crawler'; +import { RELEVANCE_TUNING_TITLE } from '../relevance_tuning'; +import { SYNONYMS_TITLE } from '../synonyms'; +import { CURATIONS_TITLE } from '../curations'; +import { RESULT_SETTINGS_TITLE } from '../result_settings'; +import { SEARCH_UI_TITLE } from '../search_ui'; +import { API_LOGS_TITLE } from '../api_logs'; import { EngineLogic } from './'; import { EngineDetails } from './types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index e8609c169855b..cbaa347d65732 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -23,17 +23,22 @@ import { EngineOverview } from '../engine_overview'; import { EngineRouter } from './'; describe('EngineRouter', () => { - const values = { dataLoading: false, engineNotFound: false, myRole: {} }; + const values = { + dataLoading: false, + engineNotFound: false, + myRole: {}, + engineName: 'some-engine', + }; const actions = { setEngineName: jest.fn(), initializeEngine: jest.fn(), clearEngine: jest.fn() }; beforeEach(() => { setMockValues(values); setMockActions(actions); + (useParams as jest.Mock).mockReturnValue({ engineName: 'some-engine' }); }); describe('useEffect', () => { beforeEach(() => { - (useParams as jest.Mock).mockReturnValue({ engineName: 'some-engine' }); shallow(); }); @@ -45,15 +50,14 @@ describe('EngineRouter', () => { expect(actions.initializeEngine).toHaveBeenCalled(); }); - it('clears engine on unmount', () => { + it('clears engine on unmount and on update', () => { unmountHandler(); expect(actions.clearEngine).toHaveBeenCalled(); }); }); it('redirects to engines list and flashes an error if the engine param was not found', () => { - (useParams as jest.Mock).mockReturnValue({ engineName: '404-engine' }); - setMockValues({ ...values, engineNotFound: true }); + setMockValues({ ...values, engineNotFound: true, engineName: '404-engine' }); const wrapper = shallow(); expect(wrapper.find(Redirect).prop('to')).toEqual('/engines'); @@ -68,6 +72,16 @@ describe('EngineRouter', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); + // This would happen if a user jumps around from one engine route to another. If the engine name + // on the path has changed, but we still have an engine stored in state, we do not want to load + // any route views as they would be rendering with the wrong data. + it('renders a loading component if the engine stored in state is stale', () => { + setMockValues({ ...values, engineName: 'some-engine' }); + (useParams as jest.Mock).mockReturnValue({ engineName: 'some-new-engine' }); + const wrapper = shallow(); + expect(wrapper.find(Loading)).toHaveLength(1); + }); + it('renders a default engine overview', () => { const wrapper = shallow(); @@ -76,7 +90,7 @@ describe('EngineRouter', () => { }); it('renders an analytics view', () => { - setMockValues({ myRole: { canViewEngineAnalytics: true } }); + setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="AnalyticsTODO"]')).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 9e0b043a87364..1d2f3f640f341 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -33,18 +33,7 @@ import { } from '../../routes'; import { ENGINES_TITLE } from '../engines'; import { OVERVIEW_TITLE } from '../engine_overview'; -import { - ANALYTICS_TITLE, - // DOCUMENTS_TITLE, - // SCHEMA_TITLE, - // CRAWLER_TITLE, - // RELEVANCE_TUNING_TITLE, - // SYNONYMS_TITLE, - // CURATIONS_TITLE, - // RESULT_SETTINGS_TITLE, - // SEARCH_UI_TITLE, - // API_LOGS_TITLE, -} from './constants'; +import { ANALYTICS_TITLE } from '../analytics'; import { Loading } from '../../../shared/loading'; import { EngineOverview } from '../engine_overview'; @@ -69,17 +58,15 @@ export const EngineRouter: React.FC = () => { }, } = useValues(AppLogic); - const { dataLoading, engineNotFound } = useValues(EngineLogic); + const { engineName: engineNameFromUrl } = useParams() as { engineName: string }; + const { engineName, dataLoading, engineNotFound } = useValues(EngineLogic); const { setEngineName, initializeEngine, clearEngine } = useActions(EngineLogic); - const { engineName } = useParams() as { engineName: string }; - const engineBreadcrumb = [ENGINES_TITLE, engineName]; - useEffect(() => { - setEngineName(engineName); + setEngineName(engineNameFromUrl); initializeEngine(); return clearEngine; - }, [engineName]); + }, [engineNameFromUrl]); if (engineNotFound) { setQueuedErrorMessage( @@ -91,7 +78,10 @@ export const EngineRouter: React.FC = () => { return ; } - if (dataLoading) return ; + const isLoadingNewEngine = engineName !== engineNameFromUrl; + if (isLoadingNewEngine || dataLoading) return ; + + const engineBreadcrumb = [ENGINES_TITLE, engineName]; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts index d35bde20f4f1e..2063f706a4741 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -48,10 +48,7 @@ describe('EngineOverviewLogic', () => { timeoutId: null, }; - const mount = () => { - resetContext({}); - EngineOverviewLogic.mount(); - }; + const { mount, unmount } = new LogicMounter(EngineOverviewLogic); beforeEach(() => { jest.clearAllMocks(); @@ -141,12 +138,9 @@ describe('EngineOverviewLogic', () => { }); describe('unmount', () => { - let unmount: Function; - beforeEach(() => { jest.useFakeTimers(); - resetContext({}); - unmount = EngineOverviewLogic.mount(); + mount(); }); it('clears existing polling timeouts on unmount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 1b222cfaacf7c..24d2fea973e14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -20,13 +20,13 @@ import { Result } from '../result/result'; export const Library: React.FC = () => { const props = { + isMetaEngine: false, result: { id: { raw: '1', }, _meta: { id: '1', - scopedId: '1', score: 100, engine: 'my-engine', }, @@ -98,6 +98,7 @@ export const Library: React.FC = () => { { { { { { }, _meta: { id: 'my-id-is-a-really-long-id-yes-it-is', - scopedId: '2', score: 100, engine: 'my-engine-is-a-really-long-engin-name-yes-it-is', }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts new file mode 100644 index 0000000000000..b993b017f7b34 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const RELEVANCE_TUNING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.title', + { defaultMessage: 'Relevance Tuning' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts new file mode 100644 index 0000000000000..40f3ddbf2899b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RELEVANCE_TUNING_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss index 8342061ee00c3..f69acbdaba150 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss @@ -1,17 +1,43 @@ .appSearchResult { - display: flex; + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: 1fr auto; + grid-template-areas: + 'content actions' + 'toggle actions'; + overflow: hidden; // Prevents child background-colors from clipping outside of panel border-radius &__content { + grid-area: content; width: 100%; padding: $euiSize; overflow: hidden; color: $euiTextColor; } - &__hiddenFieldsIndicator { + &__hiddenFieldsToggle { + grid-area: toggle; + display: flex; + justify-content: center; + padding: $euiSizeS; + border-top: $euiBorderThin; font-size: $euiFontSizeXS; - color: $euiColorDarkShade; - margin-top: $euiSizeS; + color: $euiColorPrimary; + + &:hover, + &:focus { + background-color: $euiPageBackgroundColor; + } + + .euiIcon { + margin-left: $euiSizeXS; + } + } + + &__actionButtons { + grid-area: actions; + display: flex; + flex-wrap: no-wrap; } &__actionButton { @@ -22,10 +48,27 @@ border-left: $euiBorderThin; &:hover, - &:focus, - &:active { + &:focus { background-color: $euiPageBackgroundColor; - cursor: pointer; } } } + +/** + * CSS for hover specific logic + * It's mildly horrific, so I pulled it out to its own section here + */ + +.appSearchResult--link { + &:hover, + &:focus { + @include euiSlightShadowHover; + } +} +.appSearchResult__content--link:hover { + cursor: pointer; + + & ~ .appSearchResult__actionButtons .appSearchResult__actionButton--link { + background-color: $euiPageBackgroundColor; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index 5b598a0b8565e..973fc6226910a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -18,6 +18,7 @@ import { Result } from './result'; describe('Result', () => { const props = { + isMetaEngine: false, result: { id: { raw: '1', @@ -33,7 +34,6 @@ describe('Result', () => { }, _meta: { id: '1', - scopedId: '1', score: 100, engine: 'my-engine', }, @@ -49,6 +49,7 @@ describe('Result', () => { it('renders', () => { const wrapper = shallow(); expect(wrapper.find(EuiPanel).exists()).toBe(true); + expect(wrapper.find(EuiPanel).prop('title')).toEqual('Document 1'); }); it('should render a ResultField for each field except id and _meta', () => { @@ -60,30 +61,36 @@ describe('Result', () => { ]); }); - it('passes through showScore and resultMeta to ResultHeader', () => { - const wrapper = shallow(); - expect(wrapper.find(ResultHeader).prop('showScore')).toBe(true); - expect(wrapper.find(ResultHeader).prop('resultMeta')).toEqual({ - id: '1', - scopedId: '1', - score: 100, - engine: 'my-engine', + it('passes showScore, resultMeta, and isMetaEngine to ResultHeader', () => { + const wrapper = shallow(); + expect(wrapper.find(ResultHeader).props()).toEqual({ + isMetaEngine: true, + showScore: true, + resultMeta: { + id: '1', + score: 100, + engine: 'my-engine', + }, }); }); describe('document detail link', () => { it('will render a link if shouldLinkToDetailPage is true', () => { const wrapper = shallow(); - expect(wrapper.find(ReactRouterHelper).prop('to')).toEqual('/engines/my-engine/documents/1'); - expect(wrapper.find('article.appSearchResult__content').exists()).toBe(false); - expect(wrapper.find('a.appSearchResult__content').exists()).toBe(true); + wrapper.find(ReactRouterHelper).forEach((link) => { + expect(link.prop('to')).toEqual('/engines/my-engine/documents/1'); + }); + expect(wrapper.hasClass('appSearchResult--link')).toBe(true); + expect(wrapper.find('.appSearchResult__content--link').exists()).toBe(true); + expect(wrapper.find('.appSearchResult__actionButton--link').exists()).toBe(true); }); it('will not render a link if shouldLinkToDetailPage is not set', () => { const wrapper = shallow(); expect(wrapper.find(ReactRouterHelper).exists()).toBe(false); - expect(wrapper.find('article.appSearchResult__content').exists()).toBe(true); - expect(wrapper.find('a.appSearchResult__content').exists()).toBe(false); + expect(wrapper.hasClass('appSearchResult--link')).toBe(false); + expect(wrapper.find('.appSearchResult__content--link').exists()).toBe(false); + expect(wrapper.find('.appSearchResult__actionButton--link').exists()).toBe(false); }); }); @@ -100,6 +107,7 @@ describe('Result', () => { describe('when there are more than 5 fields', () => { const propsWithMoreFields = { + isMetaEngine: false, result: { id: { raw: '1', @@ -124,7 +132,6 @@ describe('Result', () => { }, _meta: { id: '1', - scopedId: '1', score: 100, engine: 'my-engine', }, @@ -138,18 +145,16 @@ describe('Result', () => { wrapper = shallow(); }); - it('renders a collapse button', () => { - expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); + it('renders a hidden fields toggle button', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').exists()).toBe(true); }); - it('does not render an expand button', () => { - expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); + it('renders a collapse icon', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); }); - it('renders a hidden fields indicator', () => { - expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').text()).toEqual( - '1 more fields' - ); + it('does not render an expand icon', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); }); it('shows no more than 5 fields', () => { @@ -162,20 +167,22 @@ describe('Result', () => { beforeAll(() => { wrapper = shallow(); - expect(wrapper.find('.appSearchResult__actionButton').exists()).toBe(true); - wrapper.find('.appSearchResult__actionButton').simulate('click'); + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').exists()).toBe(true); + wrapper.find('.appSearchResult__hiddenFieldsToggle').simulate('click'); }); - it('renders a collapse button', () => { - expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(true); + it('renders correct toggle text', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').text()).toEqual( + 'Hide additional fields' + ); }); - it('does not render an expand button', () => { - expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(false); + it('renders a collapse icon', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(true); }); - it('does not render a hidden fields indicator', () => { - expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').exists()).toBe(false); + it('does not render an expand icon', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(false); }); it('shows all fields', () => { @@ -188,23 +195,23 @@ describe('Result', () => { beforeAll(() => { wrapper = shallow(); - expect(wrapper.find('.appSearchResult__actionButton').exists()).toBe(true); - wrapper.find('.appSearchResult__actionButton').simulate('click'); - wrapper.find('.appSearchResult__actionButton').simulate('click'); + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').exists()).toBe(true); + wrapper.find('.appSearchResult__hiddenFieldsToggle').simulate('click'); + wrapper.find('.appSearchResult__hiddenFieldsToggle').simulate('click'); }); - it('renders a collapse button', () => { - expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); + it('renders correct toggle text', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').text()).toEqual( + 'Show 1 additional field' + ); }); - it('does not render an expand button', () => { - expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); + it('renders a collapse icon', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); }); - it('renders a hidden fields indicator', () => { - expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').text()).toEqual( - '1 more fields' - ); + it('does not render an expand icon', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); }); it('shows no more than 5 fields', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index 11415f5512380..f25eb2a4ba09e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -5,6 +5,7 @@ */ import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; import './result.scss'; @@ -20,6 +21,7 @@ import { Schema } from '../../../shared/types'; interface Props { result: ResultType; + isMetaEngine: boolean; showScore?: boolean; shouldLinkToDetailPage?: boolean; schemaForTypeHighlights?: Schema; @@ -29,6 +31,7 @@ const RESULT_CUTOFF = 5; export const Result: React.FC = ({ result, + isMetaEngine, showScore = false, shouldLinkToDetailPage = false, schemaForTypeHighlights, @@ -47,75 +50,91 @@ export const Result: React.FC = ({ if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName]; }; + const documentLink = getDocumentDetailRoute(resultMeta.engine, resultMeta.id); const conditionallyLinkedArticle = (children: React.ReactNode) => { return shouldLinkToDetailPage ? ( - - {children} + +
    + {children} +
    ) : (
    {children}
    ); }; + const classes = classNames('appSearchResult', { + 'appSearchResult--link': shouldLinkToDetailPage, + }); + return ( {conditionallyLinkedArticle( <> - -
    - {resultFields - .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) - .map(([field, value]: [string, FieldValue]) => ( - - ))} -
    - {numResults > RESULT_CUTOFF && !isOpen && ( -
    - {i18n.translate('xpack.enterpriseSearch.appSearch.result.numberOfAdditionalFields', { - defaultMessage: '{numberOfAdditionalFields} more fields', - values: { - numberOfAdditionalFields: numResults - RESULT_CUTOFF, - }, - })} -
    - )} + + {resultFields + .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) + .map(([field, value]: [string, FieldValue]) => ( + + ))} )} {numResults > RESULT_CUTOFF && ( )} +
    + {shouldLinkToDetailPage && ( + + + + + + )} +
    ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx index 95b77a0aed7bb..4ccebb90eb6fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -13,57 +13,64 @@ import { ResultHeader } from './result_header'; describe('ResultHeader', () => { const resultMeta = { id: '1', - scopedId: '1', score: 100, engine: 'my-engine', }; it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.isEmptyRender()).toBe(false); }); it('always renders an id', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1'); }); describe('score', () => { it('renders score if showScore is true ', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('[data-test-subj="ResultScore"]').prop('value')).toEqual(100); }); it('does not render score if showScore is false', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('[data-test-subj="ResultScore"]').exists()).toBe(false); }); }); describe('engine', () => { - it('renders engine name if the ids dont match, which means it is a meta engine', () => { + it('renders engine name if this is a meta engine', () => { const wrapper = shallow( ); expect(wrapper.find('[data-test-subj="ResultEngine"]').prop('value')).toBe('my-engine'); }); - it('does not render an engine name if the ids match, which means it is not a meta engine', () => { + it('does not render an engine if this is not a meta engine', () => { const wrapper = shallow( ); expect(wrapper.find('[data-test-subj="ResultEngine"]').exists()).toBe(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx index 9b83014d041dd..14e0607e1249a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx @@ -13,12 +13,11 @@ import './result_header.scss'; interface Props { showScore: boolean; + isMetaEngine: boolean; resultMeta: ResultMeta; } -export const ResultHeader: React.FC = ({ showScore, resultMeta }) => { - const showEngineLabel: boolean = resultMeta.id !== resultMeta.scopedId; - +export const ResultHeader: React.FC = ({ showScore, resultMeta, isMetaEngine }) => { return (
    {showScore && ( @@ -33,7 +32,7 @@ export const ResultHeader: React.FC = ({ showScore, resultMeta }) => { )}
    - {showEngineLabel && ( + {isMetaEngine && ( ({ @@ -53,24 +53,7 @@ describe('LogRetentionLogic', () => { isLogRetentionUpdating: false, }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - log_retention_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - LogRetentionLogic.mount(); - }; + const { mount } = new LogicMounter(LogRetentionLogic); beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts index 385831dc511da..b1476dbd171ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts @@ -26,21 +26,12 @@ export const determineTooltipContent = ( if (!logRetentionSettings.enabled) { return renderOrReturnMessage(messages.noLogging); } - if (logRetentionSettings.enabled && !ilmEnabled) { + if (!ilmEnabled) { return renderOrReturnMessage(messages.ilmDisabled); } - if ( - logRetentionSettings.enabled && - ilmEnabled && - !logRetentionSettings.retentionPolicy?.isDefault - ) { + if (!logRetentionSettings.retentionPolicy?.isDefault) { return renderOrReturnMessage(messages.customPolicy); - } - if ( - logRetentionSettings.enabled && - ilmEnabled && - logRetentionSettings.retentionPolicy?.isDefault - ) { + } else { return renderOrReturnMessage(messages.defaultPolicy); } }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/constants.ts new file mode 100644 index 0000000000000..ece979b8db00d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SYNONYMS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.synonyms.title', + { defaultMessage: 'Synonyms' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/index.ts new file mode 100644 index 0000000000000..bc4388eb1fbc4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SYNONYMS_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index a109640f09bbe..6c232dbb8c588 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -12,4 +12,5 @@ export { setErrorMessage, setQueuedSuccessMessage, setQueuedErrorMessage, + clearFlashMessages, } from './set_message_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts index c5ee8200c490d..4a5a4bb6be1f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts @@ -16,6 +16,7 @@ import { setErrorMessage, setQueuedSuccessMessage, setQueuedErrorMessage, + clearFlashMessages, } from './'; describe('Flash Message Helpers', () => { @@ -68,4 +69,10 @@ describe('Flash Message Helpers', () => { }, ]); }); + + it('clearFlashMessages()', () => { + clearFlashMessages(); + + expect(FlashMessagesLogic.values.messages).toEqual([]); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts index cb73d54fd7b1e..e054ff6e2fd5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts @@ -33,3 +33,7 @@ export const setQueuedErrorMessage = (message: string) => { message, }); }; + +export const clearFlashMessages = () => { + FlashMessagesLogic.actions.clearFlashMessages(); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx index 69176b8a139e7..dae22a47035c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx @@ -25,7 +25,9 @@ export const HiddenText: React.FC = ({ text, children }) => { defaultMessage: 'Hidden text', }); const hiddenText = isHidden ? ( - {text.replace(/./g, '•')} + + {text.replace(/./g, '•')} + ) : ( text ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx index e10d56ddc09b0..67add0ca94520 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx @@ -41,10 +41,10 @@ describe('SchemaAddFieldModal', () => { expect(wrapper.find(EuiModal)).toHaveLength(1); }); - // No matter what I try I can't get this to actually achieve coverage. it('sets loading state in useEffect', () => { setState(true); - const wrapper = mount(); + const wrapper = mount(); + wrapper.setProps({ ...errors }); const input = wrapper.find(EuiFieldText); expect(input.prop('isLoading')).toEqual(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx index 0ebd59eda5be7..60689242ab882 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx @@ -19,11 +19,23 @@ describe('WorkplaceSearchHeaderActions', () => { expect(wrapper.isEmptyRender()).toBe(true); }); + it('renders a link to the personal dashboard', () => { + externalUrl.enterpriseSearchUrl = 'http://localhost:3002'; + + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonEmpty).first().prop('href')).toEqual( + 'http://localhost:3002/ws/sources' + ); + }); + it('renders a link to the search application', () => { externalUrl.enterpriseSearchUrl = 'http://localhost:3002'; const wrapper = shallow(); - expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('http://localhost:3002/ws/search'); + expect(wrapper.find(EuiButtonEmpty).last().prop('href')).toEqual( + 'http://localhost:3002/ws/search' + ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx index fe73098f044c0..c3e5f0f24a299 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx @@ -5,26 +5,33 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import { externalUrl, getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; +import { NAV } from '../../constants'; + export const WorkplaceSearchHeaderActions: React.FC = () => { if (!externalUrl.enterpriseSearchUrl) return null; return ( - - - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.headerActions.searchApplication', { - defaultMessage: 'Go to search application', - })} - - + <> + + {NAV.PERSONAL_DASHBOARD} + + + {NAV.SEARCH} + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index ccc0fe8b38ff3..3cfdb5b13de5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -18,6 +18,6 @@ describe('WorkplaceSearchNav', () => { expect(wrapper.find(SideNav)).toHaveLength(1); expect(wrapper.find(SideNavLink).first().prop('to')).toEqual('/'); - expect(wrapper.find(SideNavLink)).toHaveLength(7); + expect(wrapper.find(SideNavLink)).toHaveLength(6); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index de6c75d60189e..944820c4b1c40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -26,32 +26,26 @@ interface Props { groupsSubNav?: React.ReactNode; } -export const WorkplaceSearchNav: React.FC = ({ sourcesSubNav, groupsSubNav }) => { - // TODO: icons - return ( - - - {NAV.OVERVIEW} - - - {NAV.SOURCES} - - - {NAV.GROUPS} - - - {NAV.ROLE_MAPPINGS} - - - {NAV.SECURITY} - - - {NAV.SETTINGS} - - - - {NAV.PERSONAL_DASHBOARD} - - - ); -}; +export const WorkplaceSearchNav: React.FC = ({ sourcesSubNav, groupsSubNav }) => ( + + + {NAV.OVERVIEW} + + + {NAV.SOURCES} + + + {NAV.GROUPS} + + + {NAV.ROLE_MAPPINGS} + + + {NAV.SECURITY} + + + {NAV.SETTINGS} + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx index 4007f7a69f77a..b411d749b8a25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -24,4 +24,10 @@ describe('SourceIcon', () => { expect(wrapper.find('.wrapped-icon')).toHaveLength(1); }); + + it('renders a full bleed icon', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiIcon).prop('type')).toEqual('test-file-stub'); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 3e828d0fe80de..74e0682db89b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -52,6 +52,9 @@ export const NAV = { defaultMessage: 'View my personal dashboard', } ), + SEARCH: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.searchApplication', { + defaultMessage: 'Go to search application', + }), }; export const MAX_TABLE_ROW_ICONS = 3; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts index d5fed4c6f97cb..5f57db40cd7e6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts @@ -6,7 +6,60 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; -import { registerDocumentRoutes } from './documents'; +import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; + +describe('documents routes', () => { + describe('POST /api/app_search/engines/{engineName}/documents', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/documents', + payload: 'body', + }); + + registerDocumentsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { documents: [{ foo: 'bar' }] }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/documents/new', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { body: { documents: [{ foo: 'bar' }] } }; + mockRouter.shouldValidate(request); + }); + + it('missing documents', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + + it('wrong document type', () => { + const request = { body: { documents: ['test'] } }; + mockRouter.shouldThrow(request); + }); + + it('non-array documents type', () => { + const request = { body: { documents: { foo: 'bar' } } }; + mockRouter.shouldThrow(request); + }); + }); + }); +}); describe('document routes', () => { describe('GET /api/app_search/engines/{engineName}/documents/{documentId}', () => { diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts index a2f4b323a91aa..60cd64b32479c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts @@ -8,6 +8,30 @@ import { schema } from '@kbn/config-schema'; import { RouteDependencies } from '../../plugin'; +export function registerDocumentsRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/engines/{engineName}/documents', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + documents: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${request.params.engineName}/documents/new`, + })(context, request, response); + } + ); +} + export function registerDocumentRoutes({ router, enterpriseSearchRequestHandler, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index f64e45c656fa1..67dcbfdc4f4d5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -9,11 +9,12 @@ import { RouteDependencies } from '../../plugin'; import { registerEnginesRoutes } from './engines'; import { registerCredentialsRoutes } from './credentials'; import { registerSettingsRoutes } from './settings'; -import { registerDocumentRoutes } from './documents'; +import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerEnginesRoutes(dependencies); registerCredentialsRoutes(dependencies); registerSettingsRoutes(dependencies); + registerDocumentsRoutes(dependencies); registerDocumentRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts index 1df7a1d6875a6..99445108b315a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -9,9 +9,11 @@ import { RouteDependencies } from '../../plugin'; import { registerOverviewRoute } from './overview'; import { registerGroupsRoutes } from './groups'; import { registerSourcesRoutes } from './sources'; +import { registerSettingsRoutes } from './settings'; export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => { registerOverviewRoute(dependencies); registerGroupsRoutes(dependencies); registerSourcesRoutes(dependencies); + registerSettingsRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts new file mode 100644 index 0000000000000..932bf5e3685e6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { + registerOrgSettingsRoute, + registerOrgSettingsCustomizeRoute, + registerOrgSettingsOauthApplicationRoute, +} from './settings'; + +describe('settings routes', () => { + describe('GET /api/workplace_search/org/settings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/settings', + payload: 'params', + }); + + registerOrgSettingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/settings', + }); + }); + }); + + describe('PUT /api/workplace_search/org/settings/customize', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/org/settings/customize', + payload: 'body', + }); + + registerOrgSettingsCustomizeRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + body: { + name: 'foo', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/settings/customize', + body: mockRequest.body, + }); + }); + }); + + describe('PUT /api/workplace_search/org/settings/oauth_application', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/org/settings/oauth_application', + payload: 'body', + }); + + registerOrgSettingsOauthApplicationRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + body: { + oauth_application: { + name: 'foo', + confidential: true, + redirect_uri: 'http://foo.bar', + }, + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/settings/oauth_application', + body: mockRequest.body, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts new file mode 100644 index 0000000000000..cdba6609eb871 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerOrgSettingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/settings', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/settings', + })(context, request, response); + } + ); +} + +export function registerOrgSettingsCustomizeRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/org/settings/customize', + validate: { + body: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/settings/customize', + body: request.body, + })(context, request, response); + } + ); +} + +export function registerOrgSettingsOauthApplicationRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/org/settings/oauth_application', + validate: { + body: schema.object({ + oauth_application: schema.object({ + name: schema.string(), + confidential: schema.boolean(), + redirect_uri: schema.string(), + }), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/settings/oauth_application', + body: request.body, + })(context, request, response); + } + ); +} + +export const registerSettingsRoutes = (dependencies: RouteDependencies) => { + registerOrgSettingsRoute(dependencies); + registerOrgSettingsCustomizeRoute(dependencies); + registerOrgSettingsOauthApplicationRoute(dependencies); +}; diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 680429a4f5946..8432fdac93a9a 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -73,6 +73,7 @@ Array [ "dashboard", "query", "url", + "search-session", ], "read": Array [ "index-pattern", @@ -83,7 +84,6 @@ Array [ "lens", "map", "tag", - "background-session", ], }, "ui": Array [ @@ -92,6 +92,7 @@ Array [ "showWriteControls", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", @@ -205,8 +206,8 @@ Array [ "search", "query", "index-pattern", - "background-session", "url", + "search-session", ], "read": Array [], }, @@ -215,6 +216,7 @@ Array [ "save", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", @@ -557,6 +559,7 @@ Array [ "dashboard", "query", "url", + "search-session", ], "read": Array [ "index-pattern", @@ -567,7 +570,6 @@ Array [ "lens", "map", "tag", - "background-session", ], }, "ui": Array [ @@ -576,6 +578,7 @@ Array [ "showWriteControls", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", @@ -689,8 +692,8 @@ Array [ "search", "query", "index-pattern", - "background-session", "url", + "search-session", ], "read": Array [], }, @@ -699,6 +702,7 @@ Array [ "save", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", diff --git a/x-pack/plugins/features/server/feature_privilege_iterator.js b/x-pack/plugins/features/server/feature_privilege_iterator.js new file mode 100644 index 0000000000000..b36cd9745de12 --- /dev/null +++ b/x-pack/plugins/features/server/feature_privilege_iterator.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// the file created to remove TS cicular dependency between features and security pluin +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export { featurePrivilegeIterator } from '../../security/server/authorization'; diff --git a/x-pack/plugins/features/server/oss_features.test.ts b/x-pack/plugins/features/server/oss_features.test.ts index a22e95105ba05..6b2ce6fd70c97 100644 --- a/x-pack/plugins/features/server/oss_features.test.ts +++ b/x-pack/plugins/features/server/oss_features.test.ts @@ -5,7 +5,8 @@ */ import { buildOSSFeatures } from './oss_features'; -import { featurePrivilegeIterator } from '../../security/server/authorization'; +// @ts-expect-error +import { featurePrivilegeIterator } from './feature_privilege_iterator'; import { KibanaFeature } from '.'; import { LicenseType } from '../../licensing/server'; diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 209e26821aedd..daa5d4b5d4219 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -28,7 +28,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS app: ['discover', 'kibana'], catalogue: ['discover'], savedObject: { - all: ['search', 'query', 'index-pattern', 'background-session'], + all: ['search', 'query', 'index-pattern'], read: [], }, ui: ['show', 'save', 'saveQuery'], @@ -71,6 +71,33 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, ], }, + { + name: i18n.translate('xpack.features.ossFeatures.discoverSearchSessionsFeatureName', { + defaultMessage: 'Store Search Sessions', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'store_search_session', + name: i18n.translate( + 'xpack.features.ossFeatures.discoverStoreSearchSessionsPrivilegeName', + { + defaultMessage: 'Store Search Sessions', + } + ), + includeIn: 'all', + savedObject: { + all: ['search-session'], + read: [], + }, + ui: ['storeSearchSession'], + }, + ], + }, + ], + }, ], }, { @@ -156,7 +183,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS 'lens', 'map', 'tag', - 'background-session', ], }, ui: ['createNew', 'show', 'showWriteControls', 'saveQuery'], @@ -210,6 +236,33 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, ], }, + { + name: i18n.translate('xpack.features.ossFeatures.dashboardSearchSessionsFeatureName', { + defaultMessage: 'Store Search Sessions', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'store_search_session', + name: i18n.translate( + 'xpack.features.ossFeatures.dashboardStoreSearchSessionsPrivilegeName', + { + defaultMessage: 'Store Search Sessions', + } + ), + includeIn: 'all', + savedObject: { + all: ['search-session'], + read: [], + }, + ui: ['storeSearchSession'], + }, + ], + }, + ], + }, ], }, { diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 857bba4c606d4..c3e2ad06fb85c 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -13,7 +13,6 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server'; import { FeatureRegistry } from './feature_registry'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { buildOSSFeatures } from './oss_features'; @@ -51,6 +50,10 @@ export interface PluginStartContract { getKibanaFeatures(): KibanaFeature[]; } +interface TimelionSetupContract { + uiEnabled: boolean; +} + /** * Represents Features Plugin instance that will be managed by the Kibana plugin system. */ diff --git a/x-pack/plugins/features/tsconfig.json b/x-pack/plugins/features/tsconfig.json new file mode 100644 index 0000000000000..1260af55fbff6 --- /dev/null +++ b/x-pack/plugins/features/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index b94c2cd12cd5f..5ba4de914c724 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; +export const ASSETS_SAVED_OBJECT_TYPE = 'epm-packages-assets'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; export const MAX_TIME_COMPLETE_INSTALL = 60000; diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 1d00855de8935..e9b11a2f5ac83 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -1274,6 +1274,15 @@ "put": { "summary": "PackagePolicies - Update", "operationId": "put-packagePolicies-packagePolicyId", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/update_package_policy" + } + } + } + }, "responses": { "200": { "description": "OK", @@ -2077,6 +2086,22 @@ "download", "path" ] + }, + "update_package_policy": { + "title": "UpdatePackagePolicy", + "allOf": [ + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/new_package_policy" + } + ] } } }, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 9ab85ab2b8232..05b5b239dc980 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -789,6 +789,11 @@ paths: put: summary: PackagePolicies - Update operationId: put-packagePolicies-packagePolicyId + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/update_package_policy' responses: '200': description: OK @@ -1323,5 +1328,13 @@ components: - format_version - download - path + update_package_policy: + title: UpdatePackagePolicy + allOf: + - type: object + properties: + version: + type: string + - $ref: '#/components/schemas/new_package_policy' security: - basicAuth: [] diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml new file mode 100644 index 0000000000000..054a0e1a48be0 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml @@ -0,0 +1,7 @@ +title: UpdatePackagePolicy +allOf: + - type: object + properties: + version: + type: string + - $ref: ./new_package_policy.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml index 3b177be3d032e..4e0315556b614 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml @@ -23,6 +23,11 @@ parameters: put: summary: PackagePolicies - Update operationId: put-packagePolicies-packagePolicyId + requestBody: + content: + application/json: + schema: + $ref: ../components/schemas/update_package_policy.yaml responses: '200': description: OK diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index a9893b170492f..77625e48dbc96 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -8,6 +8,7 @@ // TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed import { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public'; import { + ASSETS_SAVED_OBJECT_TYPE, agentAssetTypes, dataTypes, defaultPackages, @@ -268,6 +269,7 @@ export type PackageInfo = export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; installed_es: EsAssetReference[]; + package_assets: PackageAssetReference[]; es_index_patterns: Record; name: string; version: string; @@ -297,6 +299,10 @@ export type EsAssetReference = Pick & { type: ElasticsearchAssetType; }; +export type PackageAssetReference = Pick & { + type: typeof ASSETS_SAVED_OBJECT_TYPE; +}; + export type RequiredPackage = typeof requiredPackages; export type DefaultPackages = typeof defaultPackages; diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index 2fcbef75b9832..aa0761c8a39bd 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -4,8 +4,15 @@ "server": true, "ui": true, "configPath": ["xpack", "fleet"], - "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features", "cloud", "usageCollection", "home"], + "requiredPlugins": ["licensing", "data"], + "optionalPlugins": [ + "security", + "features", + "cloud", + "usageCollection", + "home", + "encryptedSavedObjects" + ], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils"] } diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx new file mode 100644 index 0000000000000..bbe7f1254a140 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.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, { memo } from 'react'; +import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; +import { useLink } from '../hooks'; +import { AGENT_SAVED_OBJECT_TYPE } from '../constants'; + +/** + * Displays the provided `count` number as a link to the Agents list if it is greater than zero + */ +export const LinkedAgentCount = memo< + Omit & { count: number; agentPolicyId: string } +>(({ count, agentPolicyId, ...otherEuiLinkProps }) => { + const { getHref } = useLink(); + return count > 0 ? ( + + {count} + + ) : ( + + {count} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_fleet_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_fleet_status.tsx index 18bcb4539c740..2bb328a51c60a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_fleet_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_fleet_status.tsx @@ -13,6 +13,7 @@ interface FleetStatusState { enabled: boolean; isLoading: boolean; isReady: boolean; + error?: Error; missingRequirements?: GetFleetStatusResponse['missing_requirements']; } @@ -44,7 +45,7 @@ export const FleetStatusProvider: React.FC = ({ children }) => { missingRequirements: res.data?.missing_requirements, })); } catch (error) { - setState((s) => ({ ...s, isLoading: true })); + setState((s) => ({ ...s, isLoading: false, error })); } } useEffect(() => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts index 1ec43f4df8c8e..ca76b65518ebe 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts @@ -8,7 +8,7 @@ export { AgentPolicyCopyProvider } from './agent_policy_copy_provider'; export { AgentPolicyDeleteProvider } from './agent_policy_delete_provider'; export { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout'; -export { LinkedAgentCount } from './linked_agent_count'; +export { LinkedAgentCount } from '../../../components/linked_agent_count'; export { ConfirmDeployAgentPolicyModal } from './confirm_deploy_modal'; export { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; export { AgentPolicyActionMenu } from './actions_menu'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/linked_agent_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/linked_agent_count.tsx deleted file mode 100644 index c602f492f74c6..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/linked_agent_count.tsx +++ /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 React, { memo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink } from '@elastic/eui'; -import { useLink } from '../../../hooks'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; - -export const LinkedAgentCount = memo<{ count: number; agentPolicyId: string }>( - ({ count, agentPolicyId }) => { - const { getHref } = useLink(); - const displayValue = ( - - ); - return count > 0 ? ( - - {displayValue} - - ) : ( - displayValue - ); - } -); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx index cac133acd4d2d..19f6be3db51b0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -51,8 +51,8 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ -

    - {from === 'edit' ? ( +

    + {from === 'edit' || from === 'package-edit' ? ( -

    +

    ); - }, [from, packageInfo]); + }, [dataTestSubj, from, packageInfo]); const pageDescription = useMemo(() => { return from === 'edit' || from === 'package-edit' ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx index b90758335dc75..16703bfe76764 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -110,7 +110,9 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { } setEnrollmentAPIKeys( - res.data.list.filter((key) => key.policy_id === selectedState.agentPolicyId) + res.data.list.filter( + (key) => key.policy_id === selectedState.agentPolicyId && key.active === true + ) ); } catch (error) { notifications.toasts.addError(error, { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx index 758131a9a4b7e..8c6163578617c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; import { PAGE_ROUTING_PATHS } from '../../constants'; -import { Loading } from '../../components'; +import { Loading, Error } from '../../components'; import { useConfig, useFleetStatus, useBreadcrumbs, useCapabilities } from '../../hooks'; import { AgentListPage } from './agent_list_page'; import { SetupPage } from './setup_page'; @@ -14,6 +15,7 @@ import { AgentDetailsPage } from './agent_details_page'; import { NoAccessPage } from './error_pages/no_access'; import { EnrollmentTokenListPage } from './enrollment_token_list_page'; import { ListLayout } from './components/list_layout'; +import { WithoutHeaderLayout } from '../../layouts'; export const FleetApp: React.FunctionComponent = () => { useBreadcrumbs('fleet'); @@ -27,6 +29,22 @@ export const FleetApp: React.FunctionComponent = () => { return ; } + if (fleetStatus.error) { + return ( + + + } + error={fleetStatus.error} + /> + + ); + } + if (fleetStatus.isReady === false) { return ( { it('should link to integration policy detail when an integration policy is clicked', async () => { await mockedApi.waitForApi(); - const firstPolicy = renderResult.getByTestId('integrationNameLink') as HTMLAnchorElement; + const firstPolicy = renderResult.getAllByTestId( + 'integrationNameLink' + )[0] as HTMLAnchorElement; expect(firstPolicy.href).toEqual( 'http://localhost/mock/app/fleet#/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc' ); }); + + it('should NOT show link for agent count if it is zero', async () => { + await mockedApi.waitForApi(); + const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; + expect(firstRowAgentCount.textContent).toEqual('0'); + expect(firstRowAgentCount.tagName).not.toEqual('A'); + }); + + it('should show link for agent count if greater than zero', async () => { + await mockedApi.waitForApi(); + const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; + expect(secondRowAgentCount.textContent).toEqual('100'); + expect(secondRowAgentCount.tagName).toEqual('A'); + }); }); }); @@ -522,8 +538,87 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos updated_at: '2020-12-09T13:46:31.013Z', updated_by: 'elastic', }, + { + id: 'e3t37031-2907-44f6-89d2-5555555555', + version: 'WrrrMiwxXQ==', + name: 'nginx-2', + description: '', + namespace: 'default', + policy_id: '125c1b70-3976-11eb-ad1c-3baa423085y6', + enabled: true, + output_id: '', + inputs: [ + { + type: 'logfile', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { paths: { value: ['/var/log/nginx/access.log*'], type: 'text' } }, + id: 'logfile-nginx.access-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/access.log*'], + exclude_files: ['.gz$'], + processors: [{ add_locale: null }], + }, + }, + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.error' }, + vars: { paths: { value: ['/var/log/nginx/error.log*'], type: 'text' } }, + id: 'logfile-nginx.error-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/error.log*'], + exclude_files: ['.gz$'], + multiline: { + pattern: '^\\d{4}\\/\\d{2}\\/\\d{2} ', + negate: true, + match: 'after', + }, + processors: [{ add_locale: null }], + }, + }, + { + enabled: false, + data_stream: { type: 'logs', dataset: 'nginx.ingress_controller' }, + vars: { paths: { value: ['/var/log/nginx/ingress.log*'], type: 'text' } }, + id: 'logfile-nginx.ingress_controller-e8a37031-2907-44f6-89d2-98bd493f60dc', + }, + ], + }, + { + type: 'nginx/metrics', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'nginx.stubstatus' }, + vars: { + period: { value: '10s', type: 'text' }, + server_status_path: { value: '/nginx_status', type: 'text' }, + }, + id: 'nginx/metrics-nginx.stubstatus-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + metricsets: ['stubstatus'], + hosts: ['http://127.0.0.1:80'], + period: '10s', + server_status_path: '/nginx_status', + }, + }, + ], + vars: { hosts: { value: ['http://127.0.0.1:80'], type: 'text' } }, + }, + ], + package: { name: 'nginx', title: 'Nginx', version: '0.3.7' }, + revision: 3, + created_at: '2020-12-09T13:46:31.013Z', + created_by: 'elastic', + updated_at: '2020-12-09T13:46:31.013Z', + updated_by: 'elastic', + }, ], - total: 1, + total: 2, page: 1, perPage: 20, }; @@ -548,8 +643,22 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos updated_by: 'elastic', agents: 0, }, + { + id: '125c1b70-3976-11eb-ad1c-3baa423085y6', + name: 'EU Healthy agents', + namespace: 'default', + description: 'Protect EU from COVID', + status: 'active', + package_policies: ['e8a37031-2907-44f6-89d2-98bd493f60cd'], + is_default: false, + monitoring_enabled: ['logs', 'metrics'], + revision: 2, + updated_at: '2020-12-09T13:46:31.840Z', + updated_by: 'elastic', + agents: 100, + }, ], - total: 1, + total: 2, page: 1, perPage: 100, }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx index 4d8cb5a16034f..c740adc4201de 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx @@ -17,10 +17,7 @@ import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; import { useGetPackageInstallStatus } from '../../hooks'; import { InstallStatus } from '../../../../types'; import { useLink } from '../../../../hooks'; -import { - AGENT_SAVED_OBJECT_TYPE, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, -} from '../../../../../../../common/constants'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants'; import { useUrlPagination } from '../../../../hooks'; import { PackagePolicyAndAgentPolicy, @@ -28,6 +25,7 @@ import { } from './use_package_policies_with_agent_policy'; import { LinkAndRevision, LinkAndRevisionProps } from '../../../../components'; import { Persona } from './persona'; +import { LinkedAgentCount } from '../../../../components/linked_agent_count'; const IntegrationDetailsLink = memo<{ packagePolicy: PackagePolicyAndAgentPolicy['packagePolicy']; @@ -66,22 +64,6 @@ const AgentPolicyDetailLink = memo<{ ); }); -const PolicyAgentListLink = memo<{ agentPolicyId: string; children: ReactNode }>( - ({ agentPolicyId, children }) => { - const { getHref } = useLink(); - return ( - - {children} - - ); - } -); - interface PackagePoliciesPanelProps { name: string; version: string; @@ -156,9 +138,12 @@ export const PackagePoliciesPanel = ({ name, version }: PackagePoliciesPanelProp width: '8ch', render({ packagePolicy, agentPolicy }: PackagePolicyAndAgentPolicy) { return ( - - {agentPolicy?.agents ?? 0} - + ); }, }, diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 5d00b96634214..dbf2fbc362a45 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -40,6 +40,7 @@ export { PACKAGE_POLICY_SAVED_OBJECT_TYPE, OUTPUT_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, + ASSETS_SAVED_OBJECT_TYPE, INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 4a3412954d50c..8ce17a00acf33 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -91,12 +91,12 @@ export interface FleetSetupDeps { } export interface FleetStartDeps { - encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + encryptedSavedObjects?: EncryptedSavedObjectsPluginStart; security?: SecurityPluginStart; } export interface FleetAppContext { - encryptedSavedObjectsStart: EncryptedSavedObjectsPluginStart; + encryptedSavedObjectsStart?: EncryptedSavedObjectsPluginStart; encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginStart; config$?: Observable; @@ -250,8 +250,7 @@ export class FleetPlugin // Conditional config routes if (config.agents.enabled) { - const isESOUsingEphemeralEncryptionKey = - deps.encryptedSavedObjects.usingEphemeralEncryptionKey; + const isESOUsingEphemeralEncryptionKey = !deps.encryptedSavedObjects; if (isESOUsingEphemeralEncryptionKey) { if (this.logger) { this.logger.warn( diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index cfcde99541f13..9ccf60dc80a5f 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -41,12 +41,13 @@ import { removeInstallation, getLimitedPackages, getInstallationObject, + getInstallation, } from '../../services/epm/packages'; import { defaultIngestErrorHandler, ingestErrorToResponseOptions } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; import { licenseService } from '../../services'; import { getArchiveEntry } from '../../services/epm/archive/cache'; -import { bufferToStream } from '../../services/epm/streams'; +import { getAsset } from '../../services/epm/archive/storage'; export const getCategoriesHandler: RequestHandler< undefined, @@ -107,32 +108,51 @@ export const getFileHandler: RequestHandler APIKeyService.invalidateAPIKey(soClient, apiKey)) - ); - } + APIKeyService.invalidateAPIKeys(soClient, apiKeys); } // Update the necessary agents diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index b9d0cf883d35c..8f67753392e65 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { createAPIKey, invalidateAPIKey } from './security'; +import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; import { normalizeKuery } from '../saved_object'; @@ -66,7 +66,7 @@ export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, export async function deleteEnrollmentApiKey(soClient: SavedObjectsClientContract, id: string) { const enrollmentApiKey = await getEnrollmentAPIKey(soClient, id); - await invalidateAPIKey(soClient, enrollmentApiKey.api_key_id); + await invalidateAPIKeys(soClient, [enrollmentApiKey.api_key_id]); await soClient.update(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id, { active: false, diff --git a/x-pack/plugins/fleet/server/services/api_keys/index.ts b/x-pack/plugins/fleet/server/services/api_keys/index.ts index 4dc398c1c0c35..bc756a311dc78 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/index.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/index.ts @@ -10,7 +10,7 @@ import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; import { createAPIKey } from './security'; import { escapeSearchQueryPhrase } from '../saved_object'; -export { invalidateAPIKey } from './security'; +export { invalidateAPIKeys } from './security'; export * from './enrollment_api_key'; export async function generateOutputApiKey( @@ -24,7 +24,16 @@ export async function generateOutputApiKey( cluster: ['monitor'], index: [ { - names: ['logs-*', 'metrics-*', 'traces-*', '.ds-logs-*', '.ds-metrics-*', '.ds-traces-*'], + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + '.ds-logs-*', + '.ds-metrics-*', + '.ds-traces-*', + '.logs-endpoint.diagnostic.collection-*', + '.ds-.logs-endpoint.diagnostic.collection-*', + ], privileges: ['write', 'create_index', 'indices:admin/auto_create'], }, ], diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts index 9a32da3cff46f..a22776435e930 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/security.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts @@ -64,7 +64,7 @@ export async function authenticate(callCluster: CallESAsCurrentUser) { } } -export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: string) { +export async function invalidateAPIKeys(soClient: SavedObjectsClientContract, ids: string[]) { const adminUser = await outputService.getAdminUser(soClient); if (!adminUser) { throw new Error('No admin user configured'); @@ -88,7 +88,7 @@ export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: try { const res = await security.authc.apiKeys.invalidate(request, { - id, + ids, }); return res; diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index bcf056c9482cb..d6b62458ed1f4 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -114,10 +114,6 @@ class AppContextService { } public getEncryptedSavedObjectsSetup() { - if (!this.encryptedSavedObjectsSetup) { - throw new Error('encryptedSavedObjectsSetup is not set'); - } - return this.encryptedSavedObjectsSetup; } diff --git a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts index 6032159fdfcc5..13d58f0c75763 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts @@ -39,6 +39,7 @@ export const getPackageInfo = (args: SharedKey) => { export const getArchivePackage = (args: SharedKey) => { const packageInfo = getPackageInfo(args); const paths = getArchiveFilelist(args); + if (!paths || !packageInfo) return undefined; return { paths, packageInfo, diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts new file mode 100644 index 0000000000000..02e7e33421737 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { extname } from 'path'; +import { uniq } from 'lodash'; +import { safeLoad } from 'js-yaml'; +import { isBinaryFile } from 'isbinaryfile'; +import mime from 'mime-types'; +import uuidv5 from 'uuid/v5'; +import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server'; +import { + ASSETS_SAVED_OBJECT_TYPE, + InstallablePackage, + InstallSource, + PackageAssetReference, + RegistryDataStream, +} from '../../../../common'; +import { ArchiveEntry, getArchiveEntry, setArchiveEntry, setArchiveFilelist } from './index'; +import { parseAndVerifyPolicyTemplates, parseAndVerifyStreams } from './validation'; +import { pkgToPkgKey } from '../registry'; + +// could be anything, picked this from https://github.com/elastic/elastic-agent-client/issues/17 +const MAX_ES_ASSET_BYTES = 4 * 1024 * 1024; + +export interface PackageAsset { + package_name: string; + package_version: string; + install_source: string; + asset_path: string; + media_type: string; + data_utf8: string; + data_base64: string; +} + +export function assetPathToObjectId(assetPath: string): string { + // uuid v5 requires a SHA-1 UUID as a namespace + // used to ensure same input produces the same id + return uuidv5(assetPath, '71403015-cdd5-404b-a5da-6c43f35cad84'); +} + +export async function archiveEntryToESDocument(opts: { + path: string; + buffer: Buffer; + name: string; + version: string; + installSource: InstallSource; +}): Promise { + const { path, buffer, name, version, installSource } = opts; + const fileExt = extname(path); + const contentType = mime.lookup(fileExt); + const mediaType = mime.contentType(contentType || fileExt); + // can use to create a data URL like `data:${mediaType};base64,${base64Data}` + + const bufferIsBinary = await isBinaryFile(buffer); + const dataUtf8 = bufferIsBinary ? '' : buffer.toString('utf8'); + const dataBase64 = bufferIsBinary ? buffer.toString('base64') : ''; + + // validation: filesize? asset type? anything else + if (dataUtf8.length > MAX_ES_ASSET_BYTES) { + throw new Error(`File at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}`); + } + + if (dataBase64.length > MAX_ES_ASSET_BYTES) { + throw new Error( + `After base64 encoding file at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}` + ); + } + + return { + package_name: name, + package_version: version, + install_source: installSource, + asset_path: path, + media_type: mediaType || '', + data_utf8: dataUtf8, + data_base64: dataBase64, + }; +} + +export async function removeArchiveEntries(opts: { + savedObjectsClient: SavedObjectsClientContract; + refs: PackageAssetReference[]; +}) { + const { savedObjectsClient, refs } = opts; + const results = await Promise.all( + refs.map((ref) => savedObjectsClient.delete(ASSETS_SAVED_OBJECT_TYPE, ref.id)) + ); + return results; +} + +export async function saveArchiveEntries(opts: { + savedObjectsClient: SavedObjectsClientContract; + paths: string[]; + packageInfo: InstallablePackage; + installSource: InstallSource; +}) { + const { savedObjectsClient, paths, packageInfo, installSource } = opts; + const bulkBody = await Promise.all( + paths.map((path) => { + const buffer = getArchiveEntry(path); + if (!buffer) throw new Error(`Could not find ArchiveEntry at ${path}`); + const { name, version } = packageInfo; + return archiveEntryToBulkCreateObject({ path, buffer, name, version, installSource }); + }) + ); + + const results = await savedObjectsClient.bulkCreate(bulkBody); + return results; +} + +export async function archiveEntryToBulkCreateObject(opts: { + path: string; + buffer: Buffer; + name: string; + version: string; + installSource: InstallSource; +}): Promise> { + const { path, buffer, name, version, installSource } = opts; + const doc = await archiveEntryToESDocument({ path, buffer, name, version, installSource }); + return { + id: assetPathToObjectId(doc.asset_path), + type: ASSETS_SAVED_OBJECT_TYPE, + attributes: doc, + }; +} +export function packageAssetToArchiveEntry(asset: PackageAsset): ArchiveEntry { + const { asset_path: path, data_utf8: utf8, data_base64: base64 } = asset; + const buffer = utf8 ? Buffer.from(utf8, 'utf8') : Buffer.from(base64, 'base64'); + + return { + path, + buffer, + }; +} + +export async function getAsset(opts: { + savedObjectsClient: SavedObjectsClientContract; + path: string; +}) { + const { savedObjectsClient, path } = opts; + const assetSavedObject = await savedObjectsClient.get( + ASSETS_SAVED_OBJECT_TYPE, + assetPathToObjectId(path) + ); + const storedAsset = assetSavedObject?.attributes; + if (!storedAsset) { + return; + } + + return storedAsset; +} + +export const getEsPackage = async ( + pkgName: string, + pkgVersion: string, + references: PackageAssetReference[], + savedObjectsClient: SavedObjectsClientContract +) => { + const pkgKey = pkgToPkgKey({ name: pkgName, version: pkgVersion }); + const bulkRes = await savedObjectsClient.bulkGet( + references.map((reference) => ({ + ...reference, + fields: ['asset_path', 'data_utf8', 'data_base64'], + })) + ); + const assets = bulkRes.saved_objects.map((so) => so.attributes); + + // add asset references to cache + const paths: string[] = []; + const entries: ArchiveEntry[] = assets.map(packageAssetToArchiveEntry); + entries.forEach(({ path, buffer }) => { + if (path && buffer) { + setArchiveEntry(path, buffer); + paths.push(path); + } + }); + setArchiveFilelist({ name: pkgName, version: pkgVersion }, paths); + // create the packageInfo + // TODO: this is mostly copied from validtion.ts, needed in case package does not exist in storage yet or is missing from cache + // we don't want to reach out to the registry again so recreate it here. should check whether it exists in packageInfoCache first + + const manifestPath = `${pkgName}-${pkgVersion}/manifest.yml`; + const soResManifest = await savedObjectsClient.get( + ASSETS_SAVED_OBJECT_TYPE, + assetPathToObjectId(manifestPath) + ); + const packageInfo = safeLoad(soResManifest.attributes.data_utf8); + + try { + const readmePath = `docs/README.md`; + await savedObjectsClient.get( + ASSETS_SAVED_OBJECT_TYPE, + assetPathToObjectId(`${pkgName}-${pkgVersion}/${readmePath}`) + ); + packageInfo.readme = `/package/${pkgName}/${pkgVersion}/${readmePath}`; + } catch (err) { + // read me doesn't exist + } + + let dataStreamPaths: string[] = []; + const dataStreams: RegistryDataStream[] = []; + paths + .filter((path) => path.startsWith(`${pkgKey}/data_stream/`)) + .forEach((path) => { + const parts = path.split('/'); + if (parts.length > 2 && parts[2]) dataStreamPaths.push(parts[2]); + }); + + dataStreamPaths = uniq(dataStreamPaths); + + await Promise.all( + dataStreamPaths.map(async (dataStreamPath) => { + const dataStreamManifestPath = `${pkgKey}/data_stream/${dataStreamPath}/manifest.yml`; + const soResDataStreamManifest = await savedObjectsClient.get( + ASSETS_SAVED_OBJECT_TYPE, + assetPathToObjectId(dataStreamManifestPath) + ); + const dataStreamManifest = safeLoad(soResDataStreamManifest.attributes.data_utf8); + const { + title: dataStreamTitle, + release, + ingest_pipeline: ingestPipeline, + type, + dataset, + } = dataStreamManifest; + const streams = parseAndVerifyStreams(dataStreamManifest, dataStreamPath); + + dataStreams.push({ + dataset: dataset || `${pkgName}.${dataStreamPath}`, + title: dataStreamTitle, + release, + package: pkgName, + ingest_pipeline: ingestPipeline || 'default', + path: dataStreamPath, + type, + streams, + }); + }) + ); + packageInfo.policy_templates = parseAndVerifyPolicyTemplates(packageInfo); + packageInfo.data_streams = dataStreams; + packageInfo.assets = paths.map((path) => { + return path.replace(`${pkgName}-${pkgVersion}`, `/package/${pkgName}/${pkgVersion}`); + }); + + return { + paths, + packageInfo, + }; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index cf5e7ae0f063c..60ba80fb45f9a 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -13,6 +13,7 @@ import { RegistryInput, RegistryStream, RegistryVarsEntry, + PackageSpecManifest, } from '../../../../common/types'; import { PackageInvalidArchiveError } from '../../../errors'; import { unpackBufferEntries } from './index'; @@ -143,7 +144,7 @@ function parseAndVerifyReadme(paths: string[], pkgName: string, pkgVersion: stri const readmePath = `${pkgName}-${pkgVersion}${readmeRelPath}`; return paths.includes(readmePath) ? `/package/${pkgName}/${pkgVersion}${readmeRelPath}` : null; } -function parseAndVerifyDataStreams( +export function parseAndVerifyDataStreams( paths: string[], pkgName: string, pkgVersion: string @@ -211,7 +212,7 @@ function parseAndVerifyDataStreams( return dataStreams; } -function parseAndVerifyStreams(manifest: any, dataStreamPath: string): RegistryStream[] { +export function parseAndVerifyStreams(manifest: any, dataStreamPath: string): RegistryStream[] { const streams: RegistryStream[] = []; const manifestStreams = manifest.streams; if (manifestStreams && manifestStreams.length > 0) { @@ -243,7 +244,7 @@ function parseAndVerifyStreams(manifest: any, dataStreamPath: string): RegistryS } return streams; } -function parseAndVerifyVars(manifestVars: any[], location: string): RegistryVarsEntry[] { +export function parseAndVerifyVars(manifestVars: any[], location: string): RegistryVarsEntry[] { const vars: RegistryVarsEntry[] = []; if (manifestVars && manifestVars.length > 0) { manifestVars.forEach((manifestVar) => { @@ -278,19 +279,23 @@ function parseAndVerifyVars(manifestVars: any[], location: string): RegistryVars } return vars; } -function parseAndVerifyPolicyTemplates(manifest: any): RegistryPolicyTemplate[] { +export function parseAndVerifyPolicyTemplates( + manifest: PackageSpecManifest +): RegistryPolicyTemplate[] { const policyTemplates: RegistryPolicyTemplate[] = []; const manifestPolicyTemplates = manifest.policy_templates; - if (manifestPolicyTemplates && manifestPolicyTemplates > 0) { + if (manifestPolicyTemplates && manifestPolicyTemplates.length > 0) { manifestPolicyTemplates.forEach((policyTemplate: any) => { const { name, title: policyTemplateTitle, description, inputs, multiple } = policyTemplate; - if (!(name && policyTemplateTitle && description && inputs)) { + if (!(name && policyTemplateTitle && description)) { throw new PackageInvalidArchiveError( - `Invalid top-level manifest: one of mandatory fields 'name', 'title', 'description', 'input' missing in policy template: ${policyTemplate}` + `Invalid top-level manifest: one of mandatory fields 'name', 'title', 'description' is missing in policy template: ${policyTemplate}` ); } - - const parsedInputs = parseAndVerifyInputs(inputs, `config template ${name}`); + let parsedInputs: RegistryInput[] | undefined = []; + if (inputs) { + parsedInputs = parseAndVerifyInputs(inputs, `config template ${name}`); + } // defaults to true if undefined, but may be explicitly set to false. let parsedMultiple = true; @@ -307,7 +312,7 @@ function parseAndVerifyPolicyTemplates(manifest: any): RegistryPolicyTemplate[] } return policyTemplates; } -function parseAndVerifyInputs(manifestInputs: any, location: string): RegistryInput[] { +export function parseAndVerifyInputs(manifestInputs: any, location: string): RegistryInput[] { const inputs: RegistryInput[] = []; if (manifestInputs && manifestInputs.length > 0) { manifestInputs.forEach((input: any) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.test.ts index 78aa17da5030c..0634e3b25f89e 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.test.ts @@ -20,3 +20,18 @@ test('getBaseName', () => { const name = getRegistryDataStreamAssetBaseName(dataStream); expect(name).toStrictEqual('logs-nginx.access'); }); + +test('getBaseName for hidden index', () => { + const dataStream: RegistryDataStream = { + dataset: 'nginx.access', + title: 'Nginx Acess Logs', + release: 'beta', + type: 'logs', + ingest_pipeline: 'default', + package: 'nginx', + path: 'access', + hidden: true, + }; + const name = getRegistryDataStreamAssetBaseName(dataStream); + expect(name).toStrictEqual('.logs-nginx.access'); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.ts index 17cd28cc8a081..a7647cc95cbaa 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/index.ts @@ -11,5 +11,6 @@ import { RegistryDataStream } from '../../../types'; * {type}-{dataset} */ export function getRegistryDataStreamAssetBaseName(dataStream: RegistryDataStream): string { - return `${dataStream.type}-${dataStream.dataset}`; + const baseName = `${dataStream.type}-${dataStream.dataset}`; + return dataStream.hidden ? `.${baseName}` : baseName; } diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts index d5077308a5301..94fa4b58cd1b8 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts @@ -8,9 +8,9 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../constants'; import { loadFieldsFromYaml, Fields, Field } from '../../fields/field'; import { dataTypes, installationStatuses } from '../../../../../common/constants'; -import { ArchivePackage, InstallSource, ValueOf } from '../../../../../common/types'; +import { ArchivePackage, Installation, InstallSource, ValueOf } from '../../../../../common/types'; import { RegistryPackage, DataType } from '../../../../types'; -import { getPackageFromSource, getPackageSavedObjects } from '../../packages/get'; +import { getInstallation, getPackageFromSource, getPackageSavedObjects } from '../../packages/get'; interface FieldFormatMap { [key: string]: FieldFormatMapItem; @@ -81,18 +81,18 @@ export async function installIndexPatterns( ); const packagesToFetch = installedPackagesSavedObjects.reduce< - Array<{ name: string; version: string; installSource: InstallSource }> - >((acc, pkgSO) => { + Array<{ name: string; version: string; installedPkg: Installation | undefined }> + >((acc, pkg) => { acc.push({ - name: pkgSO.attributes.name, - version: pkgSO.attributes.version, - installSource: pkgSO.attributes.install_source, + name: pkg.attributes.name, + version: pkg.attributes.version, + installedPkg: pkg.attributes, }); return acc; }, []); if (pkgName && pkgVersion && installSource) { - const packageToInstall = packagesToFetch.find((pkgSO) => pkgSO.name === pkgName); + const packageToInstall = packagesToFetch.find((pkg) => pkg.name === pkgName); if (packageToInstall) { // set the version to the one we want to install // if we're reinstalling the number will be the same @@ -100,7 +100,11 @@ export async function installIndexPatterns( packageToInstall.version = pkgVersion; } else { // if we're installing for the first time, add to the list - packagesToFetch.push({ name: pkgName, version: pkgVersion, installSource }); + packagesToFetch.push({ + name: pkgName, + version: pkgVersion, + installedPkg: await getInstallation({ savedObjectsClient, pkgName }), + }); } } // get each package's registry info @@ -108,7 +112,8 @@ export async function installIndexPatterns( getPackageFromSource({ pkgName: pkg.name, pkgVersion: pkg.version, - pkgInstallSource: pkg.installSource, + installedPkg: pkg.installedPkg, + savedObjectsClient, }) ); const packages = await Promise.all(packagesToFetchPromise); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index c7500a9cfeaf6..c0e2fcb12bcf8 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -5,7 +5,13 @@ */ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { InstallablePackage, InstallSource, MAX_TIME_COMPLETE_INSTALL } from '../../../../common'; +import { + InstallablePackage, + InstallSource, + PackageAssetReference, + MAX_TIME_COMPLETE_INSTALL, + ASSETS_SAVED_OBJECT_TYPE, +} from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, @@ -24,6 +30,7 @@ import { deleteKibanaSavedObjectsAssets } from './remove'; import { installTransform } from '../elasticsearch/transform/install'; import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install'; +import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; // this is only exported for testing @@ -188,11 +195,26 @@ export async function _installPackage({ if (installKibanaAssetsError) throw installKibanaAssetsError; await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); + const packageAssetResults = await saveArchiveEntries({ + savedObjectsClient, + paths, + packageInfo, + installSource, + }); + const packageAssetRefs: PackageAssetReference[] = packageAssetResults.saved_objects.map( + (result) => ({ + id: result.id, + type: ASSETS_SAVED_OBJECT_TYPE, + }) + ); + // update to newly installed version when all assets are successfully installed if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { install_version: pkgVersion, install_status: 'installed', + package_assets: packageAssetRefs, }); return [ diff --git a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts index 4ad6fc96218de..fe7b8be23b03b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -43,6 +43,7 @@ const mockInstallation: SavedObject = { id: 'test-pkg', installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + package_assets: [], es_index_patterns: { pattern: 'pattern-name' }, name: 'test package', version: '1.0.0', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 01aaf111fef84..f59b7a8484035 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -7,15 +7,11 @@ import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; import { isPackageLimited, installationStatuses } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { - ArchivePackage, - InstallSource, - RegistryPackage, - EpmPackageAdditions, -} from '../../../../common/types'; +import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; +import { getEsPackage } from '../archive/storage'; import { getArchivePackage } from '../archive'; export { getFile, SearchParams } from '../registry'; @@ -103,13 +99,10 @@ export async function getPackageInfo(options: { const getPackageRes = await getPackageFromSource({ pkgName, pkgVersion, - pkgInstallSource: - savedObject?.attributes.version === pkgVersion - ? savedObject?.attributes.install_source - : 'registry', + savedObjectsClient, + installedPkg: savedObject?.attributes, }); - const paths = getPackageRes.paths; - const packageInfo = getPackageRes.packageInfo; + const { paths, packageInfo } = getPackageRes; // add properties that aren't (or aren't yet) on the package const additions: EpmPackageAdditions = { @@ -123,28 +116,53 @@ export async function getPackageInfo(options: { return createInstallableFrom(updated, savedObject); } +interface PackageResponse { + paths: string[]; + packageInfo: ArchivePackage | RegistryPackage; +} +type GetPackageResponse = PackageResponse | undefined; + // gets package from install_source if it exists otherwise gets from registry export async function getPackageFromSource(options: { pkgName: string; pkgVersion: string; - pkgInstallSource?: InstallSource; -}): Promise<{ - paths: string[] | undefined; - packageInfo: RegistryPackage | ArchivePackage; -}> { - const { pkgName, pkgVersion, pkgInstallSource } = options; - // TODO: Check package storage before checking registry - let res; - if (pkgInstallSource === 'upload') { + installedPkg?: Installation; + savedObjectsClient: SavedObjectsClientContract; +}): Promise { + const { pkgName, pkgVersion, installedPkg, savedObjectsClient } = options; + let res: GetPackageResponse; + // if the package is installed + + if (installedPkg && installedPkg.version === pkgVersion) { + const { install_source: pkgInstallSource } = installedPkg; + // check cache res = getArchivePackage({ name: pkgName, version: pkgVersion, }); + if (!res) { + res = await getEsPackage( + pkgName, + pkgVersion, + installedPkg.package_assets, + savedObjectsClient + ); + } + // for packages not in cache or package storage and installed from registry, check registry + if (!res && pkgInstallSource === 'registry') { + try { + res = await Registry.getRegistryPackage(pkgName, pkgVersion); + // TODO: add to cache and storage here? + } catch (error) { + // treating this is a 404 as no status code returned + // in the unlikely event its missing from cache, storage, and never installed from registry + } + } } else { + // else package is not installed or installed and missing from cache and storage and installed from registry res = await Registry.getRegistryPackage(pkgName, pkgVersion); } - if (!res.packageInfo || !res.paths) - throw new Error(`package info for ${pkgName}-${pkgVersion} does not exist`); + if (!res) throw new Error(`package info for ${pkgName}-${pkgVersion} does not exist`); return { paths: res.paths, packageInfo: res.packageInfo, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts index a41511260c6e7..2dcfc7949d5e5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts @@ -15,6 +15,7 @@ const mockInstallation: SavedObject = { id: 'test-pkg', installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + package_assets: [], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', version: '1.0.0', @@ -32,6 +33,7 @@ const mockInstallationUpdateFail: SavedObject = { id: 'test-pkg', installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + package_assets: [], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', version: '1.0.0', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 48dd589dd0b8f..176bcf1381674 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -379,6 +379,7 @@ export async function createInstallation(options: { { installed_kibana: [], installed_es: [], + package_assets: [], es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 0b4a0faddf0cc..331b6bfa882da 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -24,6 +24,7 @@ import { packagePolicyService, appContextService } from '../..'; import { splitPkgKey } from '../registry'; import { deletePackageCache } from '../archive'; import { deleteIlms } from '../elasticsearch/datastream_ilm/remove'; +import { removeArchiveEntries } from '../archive/storage'; export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; @@ -49,7 +50,7 @@ export async function removeInstallation(options: { `unable to remove package with existing package policy(s) in use by agent(s)` ); - // Delete the installed assets + // Delete the installed assets. Don't include installation.package_assets. Those are irrelevant to users const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; await deleteAssets(installation, savedObjectsClient, callCluster); @@ -69,6 +70,8 @@ export async function removeInstallation(options: { version: pkgVersion, }); + await removeArchiveEntries({ savedObjectsClient, refs: installation.package_assets }); + // successful delete's in SO client return {}. return something more useful return installedAssets; } diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 90f9afe2350ea..dc4f02c94acde 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -163,7 +163,6 @@ export async function getRegistryPackage( } const packageInfo = await getInfo(name, version); - return { paths, packageInfo }; } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index f514f1ecb9ae6..c37eed1910883 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -137,7 +137,16 @@ export async function setupFleet( cluster: ['monitor', 'manage_api_key'], indices: [ { - names: ['logs-*', 'metrics-*', 'traces-*', '.ds-logs-*', '.ds-metrics-*', '.ds-traces-*'], + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + '.ds-logs-*', + '.ds-metrics-*', + '.ds-traces-*', + '.logs-endpoint.diagnostic.collection-*', + '.ds-.logs-endpoint.diagnostic.collection-*', + ], privileges: ['write', 'create_index', 'indices:admin/auto_create'], }, ], diff --git a/x-pack/plugins/runtime_fields/jest.config.js b/x-pack/plugins/grokdebugger/jest.config.js similarity index 85% rename from x-pack/plugins/runtime_fields/jest.config.js rename to x-pack/plugins/grokdebugger/jest.config.js index 9c4ec56593c8b..bf43870b5ba65 100644 --- a/x-pack/plugins/runtime_fields/jest.config.js +++ b/x-pack/plugins/grokdebugger/jest.config.js @@ -7,5 +7,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', - roots: ['/x-pack/plugins/runtime_fields'], + roots: ['/x-pack/plugins/grokdebugger'], }; diff --git a/x-pack/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js b/x-pack/plugins/grokdebugger/server/models/grokdebugger_request/grokdebugger_request.test.js similarity index 83% rename from x-pack/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js rename to x-pack/plugins/grokdebugger/server/models/grokdebugger_request/grokdebugger_request.test.js index 2e0be6001f8ca..0644a797da8bd 100644 --- a/x-pack/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js +++ b/x-pack/plugins/grokdebugger/server/models/grokdebugger_request/grokdebugger_request.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { GrokdebuggerRequest } from '../grokdebugger_request'; +import { GrokdebuggerRequest } from './grokdebugger_request'; // FAILING: https://github.com/elastic/kibana/issues/51372 describe.skip('grokdebugger_request', () => { @@ -24,18 +23,18 @@ describe.skip('grokdebugger_request', () => { describe('fromDownstreamJSON factory method', () => { it('returns correct GrokdebuggerRequest instance from downstreamRequest', () => { const grokdebuggerRequest = GrokdebuggerRequest.fromDownstreamJSON(downstreamRequest); - expect(grokdebuggerRequest.rawEvent).to.eql(downstreamRequest.rawEvent); - expect(grokdebuggerRequest.pattern).to.eql(downstreamRequest.pattern); - expect(grokdebuggerRequest.customPatterns).to.eql({}); + expect(grokdebuggerRequest.rawEvent).toEqual(downstreamRequest.rawEvent); + expect(grokdebuggerRequest.pattern).toEqual(downstreamRequest.pattern); + expect(grokdebuggerRequest.customPatterns).toEqual({}); }); it('returns correct GrokdebuggerRequest instance from downstreamRequest when custom patterns are specified', () => { const grokdebuggerRequest = GrokdebuggerRequest.fromDownstreamJSON( downstreamRequestWithCustomPatterns ); - expect(grokdebuggerRequest.rawEvent).to.eql(downstreamRequest.rawEvent); - expect(grokdebuggerRequest.pattern).to.eql(downstreamRequest.pattern); - expect(grokdebuggerRequest.customPatterns).to.eql('%{FOO:bar}'); + expect(grokdebuggerRequest.rawEvent).toEqual(downstreamRequest.rawEvent); + expect(grokdebuggerRequest.pattern).toEqual(downstreamRequest.pattern); + expect(grokdebuggerRequest.customPatterns).toEqual('%{FOO:bar}'); }); }); @@ -67,7 +66,7 @@ describe.skip('grokdebugger_request', () => { }; const grokdebuggerRequest = GrokdebuggerRequest.fromDownstreamJSON(downstreamRequest); const upstreamJson = grokdebuggerRequest.upstreamJSON; - expect(upstreamJson).to.eql(expectedUpstreamJSON); + expect(upstreamJson).toEqual(expectedUpstreamJSON); }); it('returns the upstream simulate JSON request when custom patterns are specified', () => { @@ -99,7 +98,7 @@ describe.skip('grokdebugger_request', () => { downstreamRequestWithCustomPatterns ); const upstreamJson = grokdebuggerRequest.upstreamJSON; - expect(upstreamJson).to.eql(expectedUpstreamJSON); + expect(upstreamJson).toEqual(expectedUpstreamJSON); }); }); }); diff --git a/x-pack/plugins/grokdebugger/server/models/grokdebugger_response/__tests__/grokdebugger_response.js b/x-pack/plugins/grokdebugger/server/models/grokdebugger_response/grokdebugger_response.test.js similarity index 85% rename from x-pack/plugins/grokdebugger/server/models/grokdebugger_response/__tests__/grokdebugger_response.js rename to x-pack/plugins/grokdebugger/server/models/grokdebugger_response/grokdebugger_response.test.js index 3dde3244ed19b..de550b3f9bccd 100644 --- a/x-pack/plugins/grokdebugger/server/models/grokdebugger_response/__tests__/grokdebugger_response.js +++ b/x-pack/plugins/grokdebugger/server/models/grokdebugger_response/grokdebugger_response.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { GrokdebuggerResponse } from '../grokdebugger_response'; +import { GrokdebuggerResponse } from './grokdebugger_response'; describe('grokdebugger_response', () => { describe('GrokdebuggerResponse', () => { @@ -37,8 +36,8 @@ describe('grokdebugger_response', () => { client: '55.3.244.1', }; const grokdebuggerResponse = GrokdebuggerResponse.fromUpstreamJSON(upstreamJson); - expect(grokdebuggerResponse.structuredEvent).to.eql(expectedStructuredEvent); - expect(grokdebuggerResponse.error).to.eql({}); + expect(grokdebuggerResponse.structuredEvent).toEqual(expectedStructuredEvent); + expect(grokdebuggerResponse.error).toEqual({}); }); it('returns correct GrokdebuggerResponse instance when there are valid grok parse errors', () => { @@ -61,8 +60,8 @@ describe('grokdebugger_response', () => { ], }; const grokdebuggerResponse = GrokdebuggerResponse.fromUpstreamJSON(upstreamJson); - expect(grokdebuggerResponse.structuredEvent).to.eql({}); - expect(grokdebuggerResponse.error).to.be( + expect(grokdebuggerResponse.structuredEvent).toEqual({}); + expect(grokdebuggerResponse.error).toBe( 'Provided Grok patterns do not match data in the input' ); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 5eeb336ad1108..4e1ec76c52a77 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -178,7 +178,7 @@ export const ColdPhase: FunctionComponent = () => { id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText" defaultMessage="Make the index read-only and minimize its memory footprint." />{' '} - + } fullWidth diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index ae8fecd1a1958..a777f30fd2e42 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -52,7 +52,7 @@ export const HotPhase: FunctionComponent = () => { watch: isUsingDefaultRolloverPath, }); const { isUsingRollover } = useConfigurationIssues(); - const isUsingDefaultRollover = get(formData, isUsingDefaultRolloverPath); + const isUsingDefaultRollover: boolean = get(formData, isUsingDefaultRolloverPath); const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); return ( @@ -114,7 +114,7 @@ export const HotPhase: FunctionComponent = () => { defaultMessage="Learn more" /> } - docPath="indices-rollover-index.html" + docPath="ilm-rollover.html" />

    @@ -145,145 +145,145 @@ export const HotPhase: FunctionComponent = () => { } fullWidth > -
    - path="_meta.hot.useRollover"> - {(field) => ( - <> - field.setValue(e.target.checked)} - data-test-subj="rolloverSwitch" - /> -   - + path="_meta.hot.customRollover.enabled"> + {(field) => ( + <> + field.setValue(e.target.checked)} + data-test-subj="rolloverSwitch" + /> +   + + } + /> + + )} + + {isUsingRollover && ( + <> + + {showEmptyRolloverFieldsError && ( + <> + +
    {i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
    +
    + + + )} + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.code === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + - } - /> + + + + + + + + + + + + + + + + + )} - - {isUsingRollover && ( - <> - - {showEmptyRolloverFieldsError && ( - <> - -
    {i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
    -
    - - - )} - - - - {(field) => { - const showErrorCallout = field.errors.some( - (e) => e.code === ROLLOVER_EMPTY_VALIDATION - ); - if (showErrorCallout !== showEmptyRolloverFieldsError) { - setShowEmptyRolloverFieldsError(showErrorCallout); - } - return ( - - ); - }} - - - - - - - - - - - - - - - - - - - - - - - )} -
    +

    + ) : ( +
    + )} {isUsingRollover && ( <> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx index 32bf79b023137..afff5442c585c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx @@ -21,6 +21,8 @@ export const LoadingError: FunctionComponent = ({ }) => { return ( <> + + = ({ phase }) => { id="xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableExplanationText" defaultMessage="Reduce the number of segments in your shard by merging smaller files and clearing deleted ones." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx index e5ec1d116ec6f..328587a379b76 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx @@ -36,7 +36,7 @@ export const SetPriorityInputField: FunctionComponent = ({ phase }) => { defaultMessage="Set the priority for recovering your indices after a node restart. Indices with higher priorities are recovered before indices with lower priorities." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx index f1cfbeb3692f7..c5fc31d9839bd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -37,7 +37,7 @@ export const ShrinkField: FunctionComponent = ({ phase }) => { id="xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText" defaultMessage="Shrink the index into a new index with fewer primary shards." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index a8b1680ebde07..ef69f6a545656 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -32,6 +32,19 @@ interface Props { policyName: string; } +/** + * Ensure that the JSON we get from the from has phases in the correct order. + */ +const prettifyFormJson = (policy: SerializedPolicy): SerializedPolicy => ({ + ...policy, + phases: { + hot: policy.phases.hot, + warm: policy.phases.warm, + cold: policy.phases.cold, + delete: policy.phases.delete, + }, +}); + export const PolicyJsonFlyout: React.FunctionComponent = ({ policyName, close }) => { /** * policy === undefined: we are checking validity @@ -46,7 +59,7 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({ policyName, c const updatePolicy = useCallback(async () => { setPolicy(undefined); if (await validateForm()) { - setPolicy(getFormData() as SerializedPolicy); + setPolicy(prettifyFormJson(getFormData())); } else { setPolicy(null); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts index 48ed38fc8a0d7..af59aa4b9323a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const useRolloverPath = '_meta.hot.useRollover'; +export const isUsingCustomRolloverPath = '_meta.hot.customRollover.enabled'; export const isUsingDefaultRolloverPath = '_meta.hot.isUsingDefaultRollover'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index fc7bb16877157..d945ae8bb3e4e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -240,7 +240,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage', { defaultMessage: - 'A policy name cannot start with an underscore and cannot contain a question mark or a space.', + 'A policy name cannot start with an underscore and cannot contain a comma or a space.', } ), validations: policyNameValidations, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx index 3a66abebccc1a..4ddb85899f3ac 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent, createContext, useContext } from 'react'; import { useFormData } from '../../../../shared_imports'; -import { isUsingDefaultRolloverPath, useRolloverPath } from '../constants'; +import { isUsingDefaultRolloverPath, isUsingCustomRolloverPath } from '../constants'; export interface ConfigurationIssues { /** @@ -33,14 +33,20 @@ const pathToHotPhaseSearchableSnapshot = export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ - watch: [pathToHotPhaseSearchableSnapshot, useRolloverPath, isUsingDefaultRolloverPath], + watch: [ + pathToHotPhaseSearchableSnapshot, + isUsingCustomRolloverPath, + isUsingDefaultRolloverPath, + ], }); const isUsingDefaultRollover = get(formData, isUsingDefaultRolloverPath); - const rolloverSwitchEnabled = get(formData, useRolloverPath); + // Provide default value, as path may become undefined if removed from the DOM + const isUsingCustomRollover = get(formData, isUsingCustomRolloverPath, true); + return ( { const _meta: FormInternal['_meta'] = { hot: { - useRollover: Boolean(hot?.actions?.rollover), isUsingDefaultRollover: isUsingDefaultRollover(policy), + customRollover: { + enabled: Boolean(hot?.actions?.rollover), + }, bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', readonlyEnabled: Boolean(hot?.actions?.readonly), }, @@ -53,13 +55,13 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { if (draft.phases.hot.actions.rollover.max_size) { const maxSize = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_size); draft.phases.hot.actions.rollover.max_size = maxSize.size; - draft._meta.hot.maxStorageSizeUnit = maxSize.units; + draft._meta.hot.customRollover.maxStorageSizeUnit = maxSize.units; } if (draft.phases.hot.actions.rollover.max_age) { const maxAge = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_age); draft.phases.hot.actions.rollover.max_age = maxAge.size; - draft._meta.hot.maxAgeUnit = maxAge.units; + draft._meta.hot.customRollover.maxAgeUnit = maxAge.units; } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index b494e87b0bf6f..b5abf51c29028 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -7,6 +7,7 @@ import { setAutoFreeze } from 'immer'; import { cloneDeep } from 'lodash'; import { SerializedPolicy } from '../../../../../common/types'; +import { defaultRolloverAction } from '../../../constants'; import { deserializer } from './deserializer'; import { createSerializer } from './serializer'; import { FormInternal } from '../types'; @@ -172,6 +173,20 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.actions.forcemerge).toBeUndefined(); }); + it('removes the index_codec option in the forcemerge action if it is disabled in the form', () => { + formInternal.phases.warm!.actions.forcemerge = { + max_num_segments: 22, + index_codec: 'best_compression', + }; + formInternal._meta.hot.bestCompression = false; + formInternal._meta.warm.bestCompression = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.forcemerge!.index_codec).toBeUndefined(); + expect(result.phases.warm!.actions.forcemerge!.index_codec).toBeUndefined(); + }); + it('removes the readonly action if it is disabled in hot', () => { formInternal._meta.hot.readonlyEnabled = false; @@ -188,6 +203,18 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.actions.readonly).toBeUndefined(); }); + it('allows force merge and readonly actions to be configured in hot with default rollover enabled', () => { + formInternal._meta.hot.isUsingDefaultRollover = true; + formInternal._meta.hot.bestCompression = false; + formInternal.phases.hot!.actions.forcemerge = undefined; + formInternal._meta.hot.readonlyEnabled = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.readonly).toBeUndefined(); + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + }); + it('removes set priority if it is disabled in the form', () => { delete formInternal.phases.hot!.actions.set_priority; delete formInternal.phases.warm!.actions.set_priority; @@ -220,17 +247,21 @@ describe('deserializer and serializer', () => { expect(result.phases.cold!.actions.allocate!.exclude).toBeUndefined(); }); - it('removes forcemerge and rollover config when rollover is disabled in hot phase', () => { - formInternal._meta.hot.useRollover = false; + it('removes forcemerge, readonly, and rollover config when rollover is disabled in hot phase', () => { + // These two toggles jointly control whether rollover is enabled since the default is + // for rollover to be enabled. + formInternal._meta.hot.isUsingDefaultRollover = false; + formInternal._meta.hot.customRollover.enabled = false; const result = serializer(formInternal); expect(result.phases.hot!.actions.rollover).toBeUndefined(); expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + expect(result.phases.hot!.actions.readonly).toBeUndefined(); }); it('removes min_age from warm when rollover is enabled', () => { - formInternal._meta.hot.useRollover = true; + formInternal._meta.hot.customRollover.enabled = true; formInternal._meta.warm.warmPhaseOnRollover = true; const result = serializer(formInternal); @@ -238,6 +269,15 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.min_age).toBeUndefined(); }); + it('adds default rollover configuration when enabled, but previously not configured', () => { + delete formInternal.phases.hot!.actions.rollover; + formInternal._meta.hot.isUsingDefaultRollover = true; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.rollover).toEqual(defaultRolloverAction); + }); + it('removes snapshot_repository when it is unset', () => { delete formInternal.phases.hot!.actions.searchable_snapshot; delete formInternal.phases.cold!.actions.searchable_snapshot; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index ae2432971059c..4bdf902d27b6d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -32,23 +32,25 @@ const serializers = { export const schema: FormSchema = { _meta: { hot: { - useRollover: { - defaultValue: true, - label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { - defaultMessage: 'Enable rollover', - }), - }, isUsingDefaultRollover: { defaultValue: true, label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.isUsingDefaultRollover', { defaultMessage: 'Use recommended defaults', }), }, - maxStorageSizeUnit: { - defaultValue: 'gb', - }, - maxAgeUnit: { - defaultValue: 'd', + customRollover: { + enabled: { + defaultValue: true, + label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { + defaultMessage: 'Enable rollover', + }), + }, + maxStorageSizeUnit: { + defaultValue: 'gb', + }, + maxAgeUnit: { + defaultValue: 'd', + }, }, bestCompression: { label: i18nTexts.editPolicy.bestCompressionFieldLabel, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 2a7689b42554e..f718073afa352 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -5,7 +5,6 @@ */ import { produce } from 'immer'; - import { merge, cloneDeep } from 'lodash'; import { SerializedPolicy } from '../../../../../../common/types'; @@ -29,6 +28,13 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( // Copy over all updated fields merge(draft, updatedPolicy); + /** + * Important shared values for serialization + */ + const isUsingRollover = Boolean( + _meta.hot.isUsingDefaultRollover || _meta.hot.customRollover.enabled + ); + // Next copy over all meta fields and delete any fields that have been removed // by fields exposed in the form. It is very important that we do not delete // data that the form does not control! E.g., unfollow action in hot phase. @@ -42,35 +48,55 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (draft.phases.hot?.actions) { const hotPhaseActions = draft.phases.hot.actions; - if (_meta.hot.isUsingDefaultRollover) { - hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); - } else if (hotPhaseActions.rollover && _meta.hot.useRollover) { - if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { - hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.maxAgeUnit}`; - } else { - delete hotPhaseActions.rollover.max_age; - } - if (typeof updatedPolicy.phases.hot!.actions.rollover?.max_docs !== 'number') { - delete hotPhaseActions.rollover.max_docs; - } - - if (updatedPolicy.phases.hot!.actions.rollover?.max_size) { - hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; + /** + * HOT PHASE ROLLOVER + */ + if (isUsingRollover) { + if (_meta.hot.isUsingDefaultRollover) { + hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); } else { - delete hotPhaseActions.rollover.max_size; + // Rollover may not exist if editing an existing policy with initially no rollover configured + if (!hotPhaseActions.rollover) { + hotPhaseActions.rollover = {}; + } + + // We are using user-defined, custom rollover settings. + if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { + hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.customRollover.maxAgeUnit}`; + } else { + delete hotPhaseActions.rollover.max_age; + } + + if (typeof updatedPolicy.phases.hot!.actions.rollover?.max_docs !== 'number') { + delete hotPhaseActions.rollover.max_docs; + } + + if (updatedPolicy.phases.hot!.actions.rollover?.max_size) { + hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.customRollover.maxStorageSizeUnit}`; + } else { + delete hotPhaseActions.rollover.max_size; + } } + /** + * HOT PHASE FORCEMERGE + */ if (!updatedPolicy.phases.hot!.actions?.forcemerge) { delete hotPhaseActions.forcemerge; } else if (_meta.hot.bestCompression) { hotPhaseActions.forcemerge!.index_codec = 'best_compression'; + } else { + delete hotPhaseActions.forcemerge!.index_codec; } if (_meta.hot.bestCompression && hotPhaseActions.forcemerge) { hotPhaseActions.forcemerge.index_codec = 'best_compression'; } + /** + * HOT PHASE READ-ONLY + */ if (_meta.hot.readonlyEnabled) { hotPhaseActions.readonly = hotPhaseActions.readonly ?? {}; } else { @@ -82,14 +108,23 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete hotPhaseActions.readonly; } + /** + * HOT PHASE SET PRIORITY + */ if (!updatedPolicy.phases.hot!.actions?.set_priority) { delete hotPhaseActions.set_priority; } + /** + * HOT PHASE SHRINK + */ if (!updatedPolicy.phases.hot?.actions?.shrink) { delete hotPhaseActions.shrink; } + /** + * HOT PHASE SEARCHABLE SNAPSHOT + */ if (!updatedPolicy.phases.hot!.actions?.searchable_snapshot) { delete hotPhaseActions.searchable_snapshot; } @@ -101,11 +136,16 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (_meta.warm.enabled) { draft.phases.warm!.actions = draft.phases.warm?.actions ?? {}; const warmPhase = draft.phases.warm!; - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive + + /** + * WARM PHASE MIN AGE + * + * If warm phase on rollover is enabled, delete min age field + * An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + * They are mutually exclusive + */ if ( - (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && + (!isUsingRollover || !_meta.warm.warmPhaseOnRollover) && updatedPolicy.phases.warm?.min_age ) { warmPhase.min_age = `${updatedPolicy.phases.warm!.min_age}${_meta.warm.minAgeUnit}`; @@ -113,6 +153,9 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete warmPhase.min_age; } + /** + * WARM PHASE DATA ALLOCATION + */ warmPhase.actions = serializeMigrateAndAllocateActions( _meta.warm, warmPhase.actions, @@ -120,22 +163,36 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( updatedPolicy.phases.warm?.actions?.allocate?.number_of_replicas ); + /** + * WARM PHASE FORCEMERGE + */ if (!updatedPolicy.phases.warm?.actions?.forcemerge) { delete warmPhase.actions.forcemerge; } else if (_meta.warm.bestCompression) { warmPhase.actions.forcemerge!.index_codec = 'best_compression'; + } else { + delete warmPhase.actions.forcemerge!.index_codec; } + /** + * WARM PHASE READ ONLY + */ if (_meta.warm.readonlyEnabled) { warmPhase.actions.readonly = warmPhase.actions.readonly ?? {}; } else { delete warmPhase.actions.readonly; } + /** + * WARM PHASE SET PRIORITY + */ if (!updatedPolicy.phases.warm?.actions?.set_priority) { delete warmPhase.actions.set_priority; } + /** + * WARM PHASE SHRINK + */ if (!updatedPolicy.phases.warm?.actions?.shrink) { delete warmPhase.actions.shrink; } @@ -150,10 +207,16 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( draft.phases.cold!.actions = draft.phases.cold?.actions ?? {}; const coldPhase = draft.phases.cold!; + /** + * COLD PHASE MIN AGE + */ if (updatedPolicy.phases.cold?.min_age) { coldPhase.min_age = `${updatedPolicy.phases.cold!.min_age}${_meta.cold.minAgeUnit}`; } + /** + * COLD PHASE DATA ALLOCATION + */ coldPhase.actions = serializeMigrateAndAllocateActions( _meta.cold, coldPhase.actions, @@ -161,16 +224,25 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( updatedPolicy.phases.cold?.actions?.allocate?.number_of_replicas ); + /** + * COLD PHASE FREEZE + */ if (_meta.cold.freezeEnabled) { coldPhase.actions.freeze = coldPhase.actions.freeze ?? {}; } else { delete coldPhase.actions.freeze; } + /** + * COLD PHASE SET PRIORITY + */ if (!updatedPolicy.phases.cold?.actions?.set_priority) { delete coldPhase.actions.set_priority; } + /** + * COLD PHASE SEARCHABLE SNAPSHOT + */ if (!updatedPolicy.phases.cold?.actions?.searchable_snapshot) { delete coldPhase.actions.searchable_snapshot; } @@ -183,12 +255,23 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( */ if (_meta.delete.enabled) { const deletePhase = draft.phases.delete!; + + /** + * DELETE PHASE DELETE + */ deletePhase.actions = deletePhase.actions ?? {}; deletePhase.actions.delete = deletePhase.actions.delete ?? {}; + + /** + * DELETE PHASE SEARCHABLE SNAPSHOT + */ if (updatedPolicy.phases.delete?.min_age) { deletePhase.min_age = `${updatedPolicy.phases.delete!.min_age}${_meta.delete.minAgeUnit}`; } + /** + * DELETE PHASE WAIT FOR SNAPSHOT + */ if (!updatedPolicy.phases.delete?.actions?.wait_for_snapshot) { delete deletePhase.actions.wait_for_snapshot; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 4dfd7503b9973..247f607106216 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -22,11 +22,23 @@ export interface ForcemergeFields { } interface HotPhaseMetaFields extends ForcemergeFields { - useRollover: boolean; + /** + * By default rollover is enabled with set values for max age, max size and max docs. In this policy form + * opting in to default rollover overrides custom rollover values. + */ isUsingDefaultRollover: boolean; - maxStorageSizeUnit?: string; - maxAgeUnit?: string; + readonlyEnabled: boolean; + + /** + * If a policy has defined values other than the default rollover {@link defaultRolloverAction}, we store + * them here. + */ + customRollover: { + enabled: boolean; + maxStorageSizeUnit?: string; + maxAgeUnit?: string; + }; } interface WarmPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, ForcemergeFields { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 6ba2454025beb..2897551a209b2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -71,6 +71,10 @@ describe('', () => { const templateToEdit = fixtures.getTemplate({ name: 'index_template_without_mappings', indexPatterns: ['indexPattern1'], + dataStream: { + hidden: true, + anyUnknownKey: 'should_be_kept', + }, }); beforeAll(() => { @@ -85,7 +89,7 @@ describe('', () => { testBed.component.update(); }); - it('allows you to add mappings', async () => { + test('allows you to add mappings', async () => { const { actions, find } = testBed; // Logistics await actions.completeStepOne(); @@ -98,6 +102,47 @@ describe('', () => { expect(find('fieldsListItem').length).toBe(1); }); + + test('should keep data stream configuration', async () => { + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: 'test', + indexPatterns: ['myPattern*'], + version: 1, + }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(); + + await act(async () => { + actions.clickNextButton(); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + name: 'test', + indexPatterns: ['myPattern*'], + dataStream: { + hidden: true, + anyUnknownKey: 'should_be_kept', + }, + version: 1, + _kbnMeta: { + type: 'default', + isLegacy: false, + hasDatastream: true, + }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); }); describe('with mappings', () => { diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index d1b51fe5b89bf..7b442b9dd2935 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -46,7 +46,11 @@ export interface TemplateDeserialized { name: string; }; _meta?: { [key: string]: any }; // Composable template only - dataStream?: {}; // Composable template only + // Composable template only + dataStream?: { + hidden?: boolean; + [key: string]: any; + }; _kbnMeta: { type: TemplateType; hasDatastream: boolean; diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index 5dcff0ba942e1..af3d61c8808ef 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -9,6 +9,6 @@ "requiredBundles": [ "kibanaReact", "esUiShared", - "runtimeFields" + "runtimeFieldEditor" ] } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 36f7fecbcff21..652925a977fa0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -58,7 +58,7 @@ export { RuntimeField, RuntimeFieldEditorFlyoutContent, RuntimeFieldEditorFlyoutContentProps, -} from '../../../../../runtime_fields/public'; +} from '../../../../../runtime_field_editor/public'; export { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index 89e857eec0bb3..bbc3656195470 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -55,7 +55,7 @@ function getFieldsMeta(esDocsBase: string) { ), testSubject: 'indexPatternsField', }, - dataStream: { + createDataStream: { title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.dataStreamTitle', { defaultMessage: 'Data stream', }), @@ -119,6 +119,7 @@ interface LogisticsForm { interface LogisticsFormInternal extends LogisticsForm { addMeta: boolean; + doCreateDataStream: boolean; } interface Props { @@ -132,12 +133,16 @@ function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { return { ...formData, addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), + doCreateDataStream: Boolean(formData.dataStream), }; } -function formSerializer(formData: LogisticsFormInternal): LogisticsForm { - const { addMeta, ...rest } = formData; - return rest; +function getformSerializer(initialTemplateData: LogisticsForm = {}) { + return (formData: LogisticsFormInternal): LogisticsForm => { + const { addMeta, doCreateDataStream, ...rest } = formData; + const dataStream = doCreateDataStream ? initialTemplateData.dataStream ?? {} : undefined; + return { ...rest, dataStream }; + }; } export const StepLogistics: React.FunctionComponent = React.memo( @@ -146,7 +151,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( schema: schemas.logistics, defaultValue, options: { stripEmptyFields: false }, - serializer: formSerializer, + serializer: getformSerializer(defaultValue), deserializer: formDeserializer, }); const { @@ -178,7 +183,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); }, [onChange, isFormValid, validate, getFormData]); - const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( + const { name, indexPatterns, createDataStream, order, priority, version } = getFieldsMeta( documentationService.getEsDocsBase() ); @@ -245,10 +250,10 @@ export const StepLogistics: React.FunctionComponent = React.memo( {/* Create data stream */} {isLegacy !== true && ( - + )} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index c85126f08685e..2bc146c118ba2 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -129,31 +129,12 @@ export const schemas: Record = { }, ], }, - dataStream: { + doCreateDataStream: { type: FIELD_TYPES.TOGGLE, label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.datastreamLabel', { defaultMessage: 'Create data stream', }), defaultValue: false, - serializer: (value) => { - if (value === true) { - // For now, ES expects an empty object when defining a data stream - // https://github.com/elastic/elasticsearch/pull/59317 - return {}; - } - }, - deserializer: (value) => { - if (typeof value === 'boolean') { - return value; - } - - /** - * For now, it is enough to have a "data_stream" declared on the index template - * to assume that the template creates a data stream. In the future, this condition - * might change - */ - return value !== undefined; - }, }, order: { type: FIELD_TYPES.NUMBER, diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index 18c74716a35b6..3dab4113e6965 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -20,7 +20,14 @@ export const templateSchema = schema.object({ }) ), composedOf: schema.maybe(schema.arrayOf(schema.string())), - dataStream: schema.maybe(schema.object({}, { unknowns: 'allow' })), + dataStream: schema.maybe( + schema.object( + { + hidden: schema.maybe(schema.boolean()), + }, + { unknowns: 'allow' } + ) + ), _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), ilmPolicy: schema.maybe( schema.object({ diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 016100faea601..90f556794a5d9 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -53,6 +53,7 @@ export const getTemplate = ({ order = getRandomNumber(), indexPatterns = [], template: { settings, aliases, mappings } = {}, + dataStream, hasDatastream = false, isLegacy = false, type = 'default', @@ -73,12 +74,13 @@ export const getTemplate = ({ mappings, settings, }, + dataStream, hasSettings: objHasProperties(settings), hasMappings: objHasProperties(mappings), hasAliases: objHasProperties(aliases), _kbnMeta: { type, - hasDatastream, + hasDatastream: dataStream !== undefined ? true : hasDatastream, isLegacy, }, }; diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index c505a234c7b2b..5f2e355ca3a47 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -105,29 +105,38 @@ const ThresholdRT = rt.type({ export type Threshold = rt.TypeOf; -export const CriterionRT = rt.type({ +export const criterionRT = rt.type({ field: rt.string, comparator: ComparatorRT, value: rt.union([rt.string, rt.number]), }); +export type Criterion = rt.TypeOf; -export type Criterion = rt.TypeOf; -export const criteriaRT = rt.array(CriterionRT); -export type Criteria = rt.TypeOf; +export const partialCriterionRT = rt.partial(criterionRT.props); +export type PartialCriterion = rt.TypeOf; -export const countCriteriaRT = criteriaRT; +export const countCriteriaRT = rt.array(criterionRT); export type CountCriteria = rt.TypeOf; -export const ratioCriteriaRT = rt.tuple([criteriaRT, criteriaRT]); +export const partialCountCriteriaRT = rt.array(partialCriterionRT); +export type PartialCountCriteria = rt.TypeOf; + +export const ratioCriteriaRT = rt.tuple([countCriteriaRT, countCriteriaRT]); export type RatioCriteria = rt.TypeOf; -export const TimeUnitRT = rt.union([ +export const partialRatioCriteriaRT = rt.tuple([partialCountCriteriaRT, partialCountCriteriaRT]); +export type PartialRatioCriteria = rt.TypeOf; + +export const partialCriteriaRT = rt.union([partialCountCriteriaRT, partialRatioCriteriaRT]); +export type PartialCriteria = rt.TypeOf; + +export const timeUnitRT = rt.union([ rt.literal('s'), rt.literal('m'), rt.literal('h'), rt.literal('d'), ]); -export type TimeUnit = rt.TypeOf; +export type TimeUnit = rt.TypeOf; export const timeSizeRT = rt.number; export const groupByRT = rt.array(rt.string); @@ -136,15 +145,18 @@ const RequiredAlertParamsRT = rt.type({ // NOTE: "count" would be better named as "threshold", but this would require a // migration of encrypted saved objects, so we'll keep "count" until it's problematic. count: ThresholdRT, - timeUnit: TimeUnitRT, + timeUnit: timeUnitRT, timeSize: timeSizeRT, }); +const partialRequiredAlertParamsRT = rt.partial(RequiredAlertParamsRT.props); +export type PartialRequiredAlertParams = rt.TypeOf; + const OptionalAlertParamsRT = rt.partial({ groupBy: groupByRT, }); -export const alertParamsRT = rt.intersection([ +export const countAlertParamsRT = rt.intersection([ rt.type({ criteria: countCriteriaRT, ...RequiredAlertParamsRT.props, @@ -153,8 +165,18 @@ export const alertParamsRT = rt.intersection([ ...OptionalAlertParamsRT.props, }), ]); +export type CountAlertParams = rt.TypeOf; -export type CountAlertParams = rt.TypeOf; +export const partialCountAlertParamsRT = rt.intersection([ + rt.type({ + criteria: partialCountCriteriaRT, + ...RequiredAlertParamsRT.props, + }), + rt.partial({ + ...OptionalAlertParamsRT.props, + }), +]); +export type PartialCountAlertParams = rt.TypeOf; export const ratioAlertParamsRT = rt.intersection([ rt.type({ @@ -165,13 +187,29 @@ export const ratioAlertParamsRT = rt.intersection([ ...OptionalAlertParamsRT.props, }), ]); - export type RatioAlertParams = rt.TypeOf; -export const AlertParamsRT = rt.union([alertParamsRT, ratioAlertParamsRT]); -export type AlertParams = rt.TypeOf; +export const partialRatioAlertParamsRT = rt.intersection([ + rt.type({ + criteria: partialRatioCriteriaRT, + ...RequiredAlertParamsRT.props, + }), + rt.partial({ + ...OptionalAlertParamsRT.props, + }), +]); +export type PartialRatioAlertParams = rt.TypeOf; + +export const alertParamsRT = rt.union([countAlertParamsRT, ratioAlertParamsRT]); +export type AlertParams = rt.TypeOf; + +export const partialAlertParamsRT = rt.union([ + partialCountAlertParamsRT, + partialRatioAlertParamsRT, +]); +export type PartialAlertParams = rt.TypeOf; -export const isRatioAlert = (criteria: AlertParams['criteria']): criteria is RatioCriteria => { +export const isRatioAlert = (criteria: PartialCriteria): criteria is PartialRatioCriteria => { return criteria.length > 0 && Array.isArray(criteria[0]) ? true : false; }; @@ -179,11 +217,13 @@ export const isRatioAlertParams = (params: AlertParams): params is RatioAlertPar return isRatioAlert(params.criteria); }; -export const getNumerator = (criteria: RatioCriteria): Criteria => { +export const getNumerator = (criteria: C): C[0] => { return criteria[0]; }; -export const getDenominator = (criteria: RatioCriteria): Criteria => { +export const getDenominator = ( + criteria: C +): C[1] => { return criteria[1]; }; diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 3226287d4cbde..90547e6812225 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -6,8 +6,8 @@ import * as rt from 'io-ts'; import { - criteriaRT, - TimeUnitRT, + countCriteriaRT, + timeUnitRT, timeSizeRT, groupByRT, } from '../../alerting/logs/log_threshold/types'; @@ -42,8 +42,8 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([ rt.type({ - criteria: criteriaRT, - timeUnit: TimeUnitRT, + criteria: countCriteriaRT, + timeUnit: timeUnitRT, timeSize: timeSizeRT, }), rt.partial({ diff --git a/x-pack/plugins/infra/common/utility_types.ts b/x-pack/plugins/infra/common/utility_types.ts index 93fc9b729ca74..6bd784fed9308 100644 --- a/x-pack/plugins/infra/common/utility_types.ts +++ b/x-pack/plugins/infra/common/utility_types.ts @@ -43,3 +43,6 @@ export type DeepPartial = T extends any[] interface DeepPartialArray extends Array> {} type DeepPartialObject = { [P in keyof T]+?: DeepPartial }; + +export type ObjectEntry = [keyof T, T[keyof T]]; +export type ObjectEntries = Array>; diff --git a/x-pack/plugins/infra/public/alerting/inventory/index.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts index 13ce43f77c8b0..da85f363b16ec 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts @@ -5,13 +5,21 @@ */ import { i18n } from '@kbn/i18n'; import React from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/inventory_metric_threshold/types'; +import { + InventoryMetricConditions, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server/lib/alerting/inventory_metric_threshold/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { AlertTypeParams } from '../../../../alerts/common'; import { validateMetricThreshold } from './components/validation'; -export function createInventoryMetricAlertType(): AlertTypeModel { +interface InventoryMetricAlertTypeParams extends AlertTypeParams { + criteria: InventoryMetricConditions[]; +} + +export function createInventoryMetricAlertType(): AlertTypeModel { return { id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx index b8eb73b99f45e..0744984a52eaa 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx @@ -51,6 +51,7 @@ export const AlertDropdown = () => { return ( <> diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx index 3c474ee1d0ec6..555ac905d2963 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx @@ -11,12 +11,11 @@ import { i18n } from '@kbn/i18n'; import { IFieldType } from 'src/plugins/data/public'; import { Criterion } from './criterion'; import { - AlertParams, - Comparator, - Criteria as CriteriaType, - Criterion as CriterionType, - CountCriteria as CountCriteriaType, - RatioCriteria as RatioCriteriaType, + PartialAlertParams, + PartialCountCriteria as PartialCountCriteriaType, + PartialCriteria as PartialCriteriaType, + PartialCriterion as PartialCriterionType, + PartialRatioCriteria as PartialRatioCriteriaType, isRatioAlert, getNumerator, getDenominator, @@ -25,8 +24,6 @@ import { Errors, CriterionErrors } from '../../validation'; import { ExpressionLike } from './editor'; import { CriterionPreview } from './criterion_preview_chart'; -const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; - const QueryAText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCriteriaQueryAText', { defaultMessage: 'Query A', }); @@ -37,11 +34,12 @@ const QueryBText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCrit interface SharedProps { fields: IFieldType[]; - criteria?: AlertParams['criteria']; + criteria?: PartialCriteriaType; + defaultCriterion: PartialCriterionType; errors: Errors['criteria']; - alertParams: Partial; + alertParams: PartialAlertParams; sourceId: string; - updateCriteria: (criteria: AlertParams['criteria']) => void; + updateCriteria: (criteria: PartialCriteriaType) => void; } type CriteriaProps = SharedProps; @@ -60,10 +58,10 @@ export const Criteria: React.FC = (props) => { interface CriteriaWrapperProps { alertParams: SharedProps['alertParams']; fields: SharedProps['fields']; - updateCriterion: (idx: number, params: Partial) => void; + updateCriterion: (idx: number, params: PartialCriterionType) => void; removeCriterion: (idx: number) => void; addCriterion: () => void; - criteria: CriteriaType; + criteria: PartialCountCriteriaType; errors: CriterionErrors; sourceId: SharedProps['sourceId']; isRatio?: boolean; @@ -118,29 +116,24 @@ const CriteriaWrapper: React.FC = (props) => { ); }; -interface RatioCriteriaProps { - alertParams: SharedProps['alertParams']; - fields: SharedProps['fields']; - criteria: RatioCriteriaType; - errors: Errors['criteria']; - sourceId: SharedProps['sourceId']; - updateCriteria: (criteria: AlertParams['criteria']) => void; +interface RatioCriteriaProps extends SharedProps { + criteria: PartialRatioCriteriaType; } const RatioCriteria: React.FC = (props) => { - const { criteria, errors, updateCriteria } = props; + const { criteria, defaultCriterion, errors, updateCriteria } = props; const handleUpdateNumeratorCriteria = useCallback( - (criteriaParam: CriteriaType) => { - const nextCriteria: RatioCriteriaType = [criteriaParam, getDenominator(criteria)]; + (criteriaParam: PartialCountCriteriaType) => { + const nextCriteria: PartialRatioCriteriaType = [criteriaParam, getDenominator(criteria)]; updateCriteria(nextCriteria); }, [updateCriteria, criteria] ); const handleUpdateDenominatorCriteria = useCallback( - (criteriaParam: CriteriaType) => { - const nextCriteria: RatioCriteriaType = [getNumerator(criteria), criteriaParam]; + (criteriaParam: PartialCountCriteriaType) => { + const nextCriteria: PartialRatioCriteriaType = [getNumerator(criteria), criteriaParam]; updateCriteria(nextCriteria); }, [updateCriteria, criteria] @@ -150,13 +143,13 @@ const RatioCriteria: React.FC = (props) => { updateCriterion: updateNumeratorCriterion, addCriterion: addNumeratorCriterion, removeCriterion: removeNumeratorCriterion, - } = useCriteriaState(getNumerator(criteria), handleUpdateNumeratorCriteria); + } = useCriteriaState(getNumerator(criteria), defaultCriterion, handleUpdateNumeratorCriteria); const { updateCriterion: updateDenominatorCriterion, addCriterion: addDenominatorCriterion, removeCriterion: removeDenominatorCriterion, - } = useCriteriaState(getDenominator(criteria), handleUpdateDenominatorCriteria); + } = useCriteriaState(getDenominator(criteria), defaultCriterion, handleUpdateDenominatorCriteria); return ( <> @@ -191,28 +184,17 @@ const RatioCriteria: React.FC = (props) => { ); }; -interface CountCriteriaProps { - alertParams: SharedProps['alertParams']; - fields: SharedProps['fields']; - criteria: CountCriteriaType; - errors: Errors['criteria']; - sourceId: SharedProps['sourceId']; - updateCriteria: (criteria: AlertParams['criteria']) => void; +interface CountCriteriaProps extends SharedProps { + criteria: PartialCountCriteriaType; } const CountCriteria: React.FC = (props) => { - const { criteria, updateCriteria, errors } = props; - - const handleUpdateCriteria = useCallback( - (criteriaParam: CriteriaType) => { - updateCriteria(criteriaParam); - }, - [updateCriteria] - ); + const { criteria, defaultCriterion, updateCriteria, errors } = props; const { updateCriterion, addCriterion, removeCriterion } = useCriteriaState( criteria, - handleUpdateCriteria + defaultCriterion, + updateCriteria ); return ( @@ -227,8 +209,9 @@ const CountCriteria: React.FC = (props) => { }; const useCriteriaState = ( - criteria: CriteriaType, - onUpdateCriteria: (criteria: CriteriaType) => void + criteria: PartialCountCriteriaType, + defaultCriterion: PartialCriterionType, + onUpdateCriteria: (criteria: PartialCountCriteriaType) => void ) => { const updateCriterion = useCallback( (idx, criterionParams) => { @@ -241,13 +224,13 @@ const useCriteriaState = ( ); const addCriterion = useCallback(() => { - const nextCriteria = criteria ? [...criteria, DEFAULT_CRITERIA] : [DEFAULT_CRITERIA]; + const nextCriteria = [...criteria, defaultCriterion]; onUpdateCriteria(nextCriteria); - }, [criteria, onUpdateCriteria]); + }, [criteria, defaultCriterion, onUpdateCriteria]); const removeCriterion = useCallback( (idx) => { - const nextCriteria = criteria.filter((criterion, index) => { + const nextCriteria = criteria.filter((_criterion, index) => { return index !== idx; }); onUpdateCriteria(nextCriteria); diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx index b2992ead3ea1b..9763a973d2fbd 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx @@ -90,7 +90,7 @@ const getFieldInfo = (fields: IFieldType[], fieldName: string): IFieldType | und interface Props { idx: number; fields: IFieldType[]; - criterion: CriterionType; + criterion: Partial; updateCriterion: (idx: number, params: Partial) => void; removeCriterion: (idx: number) => void; canDelete: boolean; @@ -116,7 +116,11 @@ export const Criterion: React.FC = ({ }, [fields]); const fieldInfo: IFieldType | undefined = useMemo(() => { - return getFieldInfo(fields, criterion.field); + if (criterion.field) { + return getFieldInfo(fields, criterion.field); + } else { + return undefined; + } }, [fields, criterion]); const compatibleComparatorOptions = useMemo(() => { @@ -129,10 +133,8 @@ export const Criterion: React.FC = ({ const nextFieldInfo = getFieldInfo(fields, fieldName); // If the field information we're dealing with has changed, reset the comparator and value. if ( - fieldInfo && - nextFieldInfo && - (fieldInfo.type !== nextFieldInfo.type || - fieldInfo.aggregatable !== nextFieldInfo.aggregatable) + fieldInfo?.type !== nextFieldInfo?.type || + fieldInfo?.aggregatable !== nextFieldInfo?.aggregatable ) { const compatibleComparators = getCompatibleComparatorsForField(nextFieldInfo); updateCriterion(idx, { @@ -160,7 +162,7 @@ export const Criterion: React.FC = ({ idx === 0 ? firstCriterionFieldPrefix : successiveCriterionFieldPrefix } uppercase={true} - value={criterion.field} + value={criterion.field ?? 'a chosen field'} isActive={isFieldPopoverOpen} color={errors.field.length === 0 ? 'secondary' : 'danger'} onClick={(e) => { @@ -180,7 +182,8 @@ export const Criterion: React.FC = ({ 0} error={errors.field}> @@ -194,9 +197,11 @@ export const Criterion: React.FC = ({ button={ = ({ 0} error={errors.comparator}> updateCriterion(idx, { comparator: e.target.value as Comparator }) diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 47dc419022880..cb759afa66d5c 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -34,7 +34,7 @@ import { NUM_BUCKETS, } from '../../../common/criterion_preview_chart/criterion_preview_chart'; import { - AlertParams, + PartialAlertParams, Threshold, Criterion, Comparator, @@ -50,7 +50,7 @@ import { decodeOrThrow } from '../../../../../common/runtime_types'; const GROUP_LIMIT = 5; interface Props { - alertParams: Partial; + alertParams: PartialAlertParams; chartCriterion: Partial; sourceId: string; showThreshold: boolean; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index 854363aacca5c..82f491c389029 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -4,25 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState } from 'react'; +import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; import useMount from 'react-use/lib/useMount'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression'; -import { ForLastExpression } from '../../../../../../triggers_actions_ui/public'; import { - AlertParams, + AlertTypeParamsExpressionProps, + ForLastExpression, +} from '../../../../../../triggers_actions_ui/public'; +import { Comparator, - ThresholdType, isRatioAlert, + PartialAlertParams, + PartialCountAlertParams, + PartialCriteria as PartialCriteriaType, + PartialRatioAlertParams, + ThresholdType, + timeUnitRT, } from '../../../../../common/alerting/logs/log_threshold/types'; -import { Threshold } from './threshold'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { ObjectEntries } from '../../../../../common/utility_types'; +import { + LogIndexField, + LogSourceProvider, + useLogSourceContext, +} from '../../../../containers/logs/log_source'; +import { useSourceId } from '../../../../containers/source_id'; +import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression'; +import { errorsRT } from '../../validation'; import { Criteria } from './criteria'; +import { Threshold } from './threshold'; import { TypeSwitcher } from './type_switcher'; -import { useSourceId } from '../../../../containers/source_id'; -import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source'; -import { Errors } from '../../validation'; export interface ExpressionCriteria { field?: string; @@ -34,45 +47,50 @@ interface LogsContextMeta { isInternal?: boolean; } -interface Props { - errors: Errors; - alertParams: Partial; - setAlertParams(key: string, value: any): void; - setAlertProperty(key: string, value: any): void; - sourceId: string; - metadata: LogsContextMeta; -} - -const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; - const DEFAULT_BASE_EXPRESSION = { timeSize: 5, - timeUnit: 'm', + timeUnit: 'm' as const, }; -const DEFAULT_COUNT_EXPRESSION = { +const DEFAULT_FIELD = 'log.level'; + +const createDefaultCriterion = ( + availableFields: LogIndexField[], + value: ExpressionCriteria['value'] +) => + availableFields.some((availableField) => availableField.name === DEFAULT_FIELD) + ? { field: DEFAULT_FIELD, comparator: Comparator.EQ, value } + : { field: undefined, comparator: undefined, value: undefined }; + +const createDefaultCountAlertParams = ( + availableFields: LogIndexField[] +): PartialCountAlertParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 75, comparator: Comparator.GT, }, - criteria: [DEFAULT_CRITERIA], -}; + criteria: [createDefaultCriterion(availableFields, 'error')], +}); -const DEFAULT_RATIO_EXPRESSION = { +const createDefaultRatioAlertParams = ( + availableFields: LogIndexField[] +): PartialRatioAlertParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 2, comparator: Comparator.GT, }, criteria: [ - [DEFAULT_CRITERIA], - [{ field: 'log.level', comparator: Comparator.EQ, value: 'warning' }], + [createDefaultCriterion(availableFields, 'error')], + [createDefaultCriterion(availableFields, 'warning')], ], -}; +}); -export const ExpressionEditor: React.FC = (props) => { - const isInternal = props.metadata?.isInternal; +export const ExpressionEditor: React.FC< + AlertTypeParamsExpressionProps +> = (props) => { + const isInternal = props.metadata?.isInternal ?? false; const [sourceId] = useSourceId(); const { http } = useKibana().services; @@ -80,12 +98,12 @@ export const ExpressionEditor: React.FC = (props) => { <> {isInternal ? ( - + ) : ( - + )} @@ -93,7 +111,7 @@ export const ExpressionEditor: React.FC = (props) => { ); }; -export const SourceStatusWrapper: React.FC = (props) => { +export const SourceStatusWrapper: React.FC = ({ children }) => { const { initialize, isLoadingSourceStatus, @@ -101,7 +119,6 @@ export const SourceStatusWrapper: React.FC = (props) => { hasFailedLoadingSourceStatus, loadSourceStatus, } = useLogSourceContext(); - const { children } = props; useMount(() => { initialize(); @@ -136,16 +153,19 @@ export const SourceStatusWrapper: React.FC = (props) => { ); }; -export const Editor: React.FC = (props) => { - const { setAlertParams, alertParams, errors, sourceId } = props; +export const Editor: React.FC< + AlertTypeParamsExpressionProps +> = (props) => { + const { setAlertParams, alertParams, errors } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); - const { sourceStatus } = useLogSourceContext(); - useMount(() => { - for (const [key, value] of Object.entries({ ...DEFAULT_COUNT_EXPRESSION, ...alertParams })) { - setAlertParams(key, value); - } - setHasSetDefaults(true); - }); + const { sourceId, sourceStatus } = useLogSourceContext(); + + const { + criteria: criteriaErrors, + threshold: thresholdErrors, + timeSizeUnit: timeSizeUnitErrors, + timeWindowSize: timeWindowSizeErrors, + } = useMemo(() => decodeOrThrow(errorsRT)(errors), [errors]); const supportedFields = useMemo(() => { if (sourceStatus?.logIndexFields) { @@ -176,7 +196,7 @@ export const Editor: React.FC = (props) => { ); const updateCriteria = useCallback( - (criteria: AlertParams['criteria']) => { + (criteria: PartialCriteriaType) => { setAlertParams('criteria', criteria); }, [setAlertParams] @@ -191,7 +211,9 @@ export const Editor: React.FC = (props) => { const updateTimeUnit = useCallback( (tu: string) => { - setAlertParams('timeUnit', tu); + if (timeUnitRT.is(tu)) { + setAlertParams('timeUnit', tu); + } }, [setAlertParams] ); @@ -203,20 +225,31 @@ export const Editor: React.FC = (props) => { [setAlertParams] ); + const defaultCountAlertParams = useMemo(() => createDefaultCountAlertParams(supportedFields), [ + supportedFields, + ]); + const updateType = useCallback( (type: ThresholdType) => { - const defaults = type === 'count' ? DEFAULT_COUNT_EXPRESSION : DEFAULT_RATIO_EXPRESSION; + const defaults = + type === 'count' ? defaultCountAlertParams : createDefaultRatioAlertParams(supportedFields); // Reset properties that don't make sense switching from one context to the other - for (const [key, value] of Object.entries({ - criteria: defaults.criteria, - count: defaults.count, - })) { - setAlertParams(key, value); - } + setAlertParams('count', defaults.count); + setAlertParams('criteria', defaults.criteria); }, - [setAlertParams] + [defaultCountAlertParams, setAlertParams, supportedFields] ); + useMount(() => { + const newAlertParams = { ...defaultCountAlertParams, ...alertParams }; + for (const [key, value] of Object.entries(newAlertParams) as ObjectEntries< + typeof newAlertParams + >) { + setAlertParams(key, value); + } + setHasSetDefaults(true); + }); + // Wait until the alert param defaults have been set if (!hasSetDefaults) return null; @@ -224,7 +257,8 @@ export const Editor: React.FC = (props) => { = (props) => { comparator={alertParams.count?.comparator} value={alertParams.count?.value} updateThreshold={updateThreshold} - errors={errors.threshold} + errors={thresholdErrors} /> = (props) => { timeWindowUnit={alertParams.timeUnit} onChangeWindowSize={updateTimeSize} onChangeWindowUnit={updateTimeUnit} - errors={{ timeWindowSize: errors.timeWindowSize, timeSizeUnit: errors.timeSizeUnit }} + errors={{ timeWindowSize: timeWindowSizeErrors, timeSizeUnit: timeSizeUnitErrors }} /> void; } -const getThresholdType = (criteria: AlertParams['criteria']): ThresholdType => { +const getThresholdType = (criteria: PartialCriteria): ThresholdType => { return isRatioAlert(criteria) ? 'ratio' : 'count'; }; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts index 7154a77496b81..6cdb81155ec9a 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts @@ -6,10 +6,13 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; -import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../common/alerting/logs/log_threshold/types'; +import { + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + PartialAlertParams, +} from '../../../common/alerting/logs/log_threshold/types'; import { validateExpression } from './validation'; -export function getAlertType(): AlertTypeModel { +export function getAlertType(): AlertTypeModel { return { id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts b/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts index 6630b3d079141..24d373558008d 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts @@ -5,45 +5,53 @@ */ import { i18n } from '@kbn/i18n'; +import * as rt from 'io-ts'; import { isNumber, isFinite } from 'lodash'; -import { ValidationResult } from '../../../../triggers_actions_ui/public'; +import { IErrorObject, ValidationResult } from '../../../../triggers_actions_ui/public'; import { - AlertParams, - Criteria, - RatioCriteria, + PartialCountCriteria, isRatioAlert, getNumerator, getDenominator, + PartialRequiredAlertParams, + PartialCriteria, } from '../../../common/alerting/logs/log_threshold/types'; -export interface CriterionErrors { - [id: string]: { - field: string[]; - comparator: string[]; - value: string[]; - }; -} +export const criterionErrorRT = rt.type({ + field: rt.array(rt.string), + comparator: rt.array(rt.string), + value: rt.array(rt.string), +}); -export interface Errors { - threshold: { - value: string[]; - }; +export const criterionErrorsRT = rt.record(rt.string, criterionErrorRT); + +export type CriterionErrors = rt.TypeOf; + +const alertingErrorRT: rt.Type = rt.recursion('AlertingError', () => + rt.record(rt.string, rt.union([rt.string, rt.array(rt.string), alertingErrorRT])) +); + +export const errorsRT = rt.type({ + threshold: rt.type({ + value: rt.array(rt.string), + }), // NOTE: The data structure for criteria errors isn't 100% // ideal but we need to conform to the interfaces that the alerting // framework expects. - criteria: { - [id: string]: CriterionErrors; - }; - timeWindowSize: string[]; - timeSizeUnit: string[]; -} + criteria: rt.record(rt.string, criterionErrorsRT), + timeWindowSize: rt.array(rt.string), + timeSizeUnit: rt.array(rt.string), +}); + +export type Errors = rt.TypeOf; export function validateExpression({ count, criteria, timeSize, - timeUnit, -}: Partial): ValidationResult { +}: PartialRequiredAlertParams & { + criteria: PartialCriteria; +}): ValidationResult { const validationResult = { errors: {} }; // NOTE: In the case of components provided by the Alerting framework the error property names @@ -79,7 +87,7 @@ export function validateExpression({ // Criteria validation if (criteria && criteria.length > 0) { - const getCriterionErrors = (_criteria: Criteria): CriterionErrors => { + const getCriterionErrors = (_criteria: PartialCountCriteria): CriterionErrors => { const _errors: CriterionErrors = {}; _criteria.forEach((criterion, idx) => { @@ -114,12 +122,12 @@ export function validateExpression({ }; if (!isRatioAlert(criteria)) { - const criteriaErrors = getCriterionErrors(criteria as Criteria); + const criteriaErrors = getCriterionErrors(criteria); errors.criteria[0] = criteriaErrors; } else { - const numeratorErrors = getCriterionErrors(getNumerator(criteria as RatioCriteria)); + const numeratorErrors = getCriterionErrors(getNumerator(criteria)); errors.criteria[0] = numeratorErrors; - const denominatorErrors = getCriterionErrors(getDenominator(criteria as RatioCriteria)); + const denominatorErrors = getCriterionErrors(getDenominator(criteria)); errors.criteria[1] = denominatorErrors; } } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts index cccd5fbc439d7..9c32c473f4597 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -8,10 +8,18 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { validateMetricThreshold } from './components/validation'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types'; +import { AlertTypeParams } from '../../../../alerts/common'; +import { + MetricExpressionParams, + METRIC_THRESHOLD_ALERT_TYPE_ID, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server/lib/alerting/metric_threshold/types'; + +interface MetricThresholdAlertTypeParams extends AlertTypeParams { + criteria: MetricExpressionParams[]; +} -export function createMetricThresholdAlertType(): AlertTypeModel { +export function createMetricThresholdAlertType(): AlertTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx index fe57b9db0e8b7..8582be008a44a 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx @@ -79,7 +79,13 @@ export const LogEntryContextMenu: React.FC = ({ return ( - + diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 879d2d95d7946..d7f40f603a9f7 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -9,6 +9,7 @@ import { useCallback, useMemo, useState } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; import type { HttpHandler } from 'src/core/public'; import { + LogIndexField, LogSourceConfiguration, LogSourceConfigurationProperties, LogSourceConfigurationPropertiesPatch, @@ -20,6 +21,7 @@ import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status'; import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration'; export { + LogIndexField, LogSourceConfiguration, LogSourceConfigurationProperties, LogSourceConfigurationPropertiesPatch, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index 98367335d9c2d..6fc9ce3d8983e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -178,21 +178,19 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent - - - - - - - - + + + + + + { )} size="s" color="primary" - iconType="plusInCircle" + iconType="indexOpen" > {ADD_DATA_LABEL} diff --git a/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx index 3f109e7383c0e..62f2d89b65fdf 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx @@ -59,7 +59,7 @@ export const LogColumnsConfigurationPanel: React.FunctionComponent @@ -182,7 +182,7 @@ const FieldLogColumnConfigurationPanel: React.FunctionComponent<{ ); return ( - +
    @@ -212,7 +212,7 @@ const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ - +
    diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 222278dde3314..24f9598484d71 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -92,7 +92,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { )} size="s" color="primary" - iconType="plusInCircle" + iconType="indexOpen" > {ADD_DATA_LABEL} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index 4718ed09dc9b2..3f0798c4a1670 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -51,7 +51,7 @@ export const ProcessRow = ({ cells, item }: Props) => { {({ measureRef, bounds: { height = 0 } }) => ( - +
    @@ -81,7 +81,7 @@ export const ProcessRow = ({ cells, item }: Props) => { )} - + {i18n.translate( @@ -92,7 +92,7 @@ export const ProcessRow = ({ cells, item }: Props) => { )} - {item.pid} + {item.pid} @@ -105,12 +105,12 @@ export const ProcessRow = ({ cells, item }: Props) => { )} - {item.user} + {item.user} - + )} @@ -120,11 +120,15 @@ export const ProcessRow = ({ cells, item }: Props) => { ); }; -export const CodeLine = euiStyled(EuiCode).attrs({ +const ExpandedRowDescriptionList = euiStyled(EuiDescriptionList).attrs({ + compressed: true, +})` + width: 100%; +`; + +const CodeListItem = euiStyled(EuiCode).attrs({ transparentBackground: true, })` - text-overflow: ellipsis; - overflow: hidden; padding: 0 !important; & code.euiCodeBlock__code { white-space: nowrap !important; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx index 7b7a285b5d6b8..af515ae75854c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx @@ -138,7 +138,7 @@ const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => { }; const ChartContainer = euiStyled.div` - width: 300px; + width: 100%; height: 140px; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 3e4b066afa157..1ea6e397e7768 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -28,7 +28,7 @@ import { FORMATTERS } from '../../../../../../../../common/formatters'; import { euiStyled } from '../../../../../../../../../observability/public'; import { SortBy } from '../../../../hooks/use_process_list'; import { Process } from './types'; -import { ProcessRow, CodeLine } from './process_row'; +import { ProcessRow } from './process_row'; import { StateBadge } from './state_badge'; import { STATE_ORDER } from './states'; @@ -150,7 +150,7 @@ export const ProcessesTable = ({ return ( <> - + {columns.map((column) => ( @@ -296,3 +296,11 @@ const columns: Array<{ render: (value: number) => FORMATTERS.percent(value), }, ]; + +const CodeLine = euiStyled.div` + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; + font-size: ${(props) => props.theme.eui.euiFontSizeS}; + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx index 6efabf1b8c0ae..5bbba906b62f2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -7,7 +7,15 @@ import React, { useMemo } from 'react'; import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, EuiLoadingSpinner, EuiBasicTableColumn } from '@elastic/eui'; +import { + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiHorizontalRule, +} from '@elastic/eui'; import { euiStyled } from '../../../../../../../../../observability/public'; import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { STATE_NAMES } from './states'; @@ -17,63 +25,51 @@ interface Props { isLoading: boolean; } -type SummaryColumn = { +type SummaryRecord = { total: number; } & Record; export const SummaryTable = ({ processSummary, isLoading }: Props) => { const processCount = useMemo( () => - [ - { - total: isLoading ? -1 : processSummary.total, - ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), - ...(isLoading ? {} : processSummary), - }, - ] as SummaryColumn[], + ({ + total: isLoading ? -1 : processSummary.total, + ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), + ...(isLoading ? {} : processSummary), + } as SummaryRecord), [processSummary, isLoading] ); return ( - - - + <> + + {Object.entries(processCount).map(([field, value]) => ( + + + {columnTitles[field as keyof SummaryRecord]} + + {value === -1 ? : value} + + + + ))} + + + ); }; -const loadingRenderer = (value: number) => (value === -1 ? : value); - -const columns = [ - { - field: 'total', - name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.headingTotalProcesses', { - defaultMessage: 'Total processes', - }), - width: 125, - render: loadingRenderer, - }, - ...Object.entries(STATE_NAMES).map(([field, name]) => ({ field, name, render: loadingRenderer })), -] as Array>; +const columnTitles = { + total: i18n.translate('xpack.infra.metrics.nodeDetails.processes.headingTotalProcesses', { + defaultMessage: 'Total processes', + }), + ...STATE_NAMES, +}; const LoadingSpinner = euiStyled(EuiLoadingSpinner).attrs({ size: 'm' })` margin-top: 2px; margin-bottom: 3px; `; -const StyleWrapper = euiStyled.div` - & .euiTableHeaderCell { - border-bottom: none; - & .euiTableCellContent { - padding-bottom: 0; - } - & .euiTableCellContent__text { - font-size: ${(props) => props.theme.eui.euiFontSizeS}; - } - } - - & .euiTableRowCell { - border-top: none; - & .euiTableCellContent { - padding-top: 0; - } - } +const ColumnTitle = euiStyled(EuiDescriptionListTitle)` + white-space: nowrap; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index d66fd44feba56..f1e796ef8ba18 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -222,9 +222,9 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible - + - + @@ -240,7 +240,7 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible - + props.theme.eui.paddingSizes.s} ${(props) => props.theme.eui.paddingSizes.m}; + @media only screen and (max-width: 767px) { + margin-top: 30px; + } `; const TimelineChartContainer = euiStyled.div` diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 1c1baad30f473..f4da68d9dead7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -186,9 +186,8 @@ export const LegendControls = ({ button={buttonComponent} > Legend Options - + @@ -243,6 +240,10 @@ export const LegendControls = ({ checked={draftLegend.reverseColors} onChange={handleReverseColors} compressed + style={{ + position: 'relative', + top: '8px', + }} /> - + { }; const Swatch = euiStyled.div` - width: 16px; + width: 15px; height: 12px; flex: 0 0 auto; &:first-child { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx index ae64188f8a469..f4cec07b53b3b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx @@ -14,7 +14,7 @@ export interface Props { export const SwatchLabel = ({ label, color }: Props) => { return ( - + diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 1941ec6326ddb..254cac0cb8e75 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -9,7 +9,12 @@ import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; -import { RecoveredActionGroup } from '../../../../../alerts/common'; +import { + ActionGroup, + AlertInstanceContext, + AlertInstanceState, + RecoveredActionGroup, +} from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; @@ -19,10 +24,11 @@ import { buildErrorAlertReason, buildFiredAlertReason, buildNoDataAlertReason, - buildRecoveredAlertReason, + // buildRecoveredAlertReason, stateToAlertMessage, } from '../common/messages'; import { evaluateCondition } from './evaluate_condition'; +import { InventoryMetricThresholdAllowedActionGroups } from './register_inventory_metric_threshold_alert_type'; interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; @@ -35,7 +41,16 @@ interface InventoryMetricThresholdParams { export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => async ({ services, params, -}: AlertExecutorOptions) => { +}: AlertExecutorOptions< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + InventoryMetricThresholdAllowedActionGroups +>) => { const { criteria, filterQuery, @@ -84,9 +99,14 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = .map((result) => buildReasonWithVerboseMetricName(result[item], buildFiredAlertReason)) .join('\n'); } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { - reason = results - .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason)) - .join('\n'); + /* + * Custom recovery actions aren't yet available in the alerting framework + * Uncomment the code below once they've been implemented + * Reference: https://github.com/elastic/kibana/issues/87048 + */ + // reason = results + // .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason)) + // .join('\n'); } if (alertOnNoData) { if (nextState === AlertStates.NO_DATA) { @@ -103,18 +123,25 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; - alertInstance.scheduleActions(actionGroupId, { - group: item, - alertState: stateToAlertMessage[nextState], - reason, - timestamp: moment().toISOString(), - value: mapToConditionsLookup(results, (result) => - formatMetric(result[item].metric, result[item].currentValue) - ), - threshold: mapToConditionsLookup(criteria, (c) => c.threshold), - metric: mapToConditionsLookup(criteria, (c) => c.metric), - }); + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS_ID; + alertInstance.scheduleActions( + /** + * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on + * the RecoveredActionGroup isn't allowed + */ + (actionGroupId as unknown) as InventoryMetricThresholdAllowedActionGroups, + { + group: item, + alertState: stateToAlertMessage[nextState], + reason, + timestamp: moment().toISOString(), + value: mapToConditionsLookup(results, (result) => + formatMetric(result[item].metric, result[item].currentValue) + ), + threshold: mapToConditionsLookup(criteria, (c) => c.threshold), + metric: mapToConditionsLookup(criteria, (c) => c.metric), + } + ); } alertInstance.replaceState({ @@ -148,8 +175,9 @@ const mapToConditionsLookup = ( {} ); -export const FIRED_ACTIONS = { - id: 'metrics.invenotry_threshold.fired', +export const FIRED_ACTIONS_ID = 'metrics.invenotry_threshold.fired'; +export const FIRED_ACTIONS: ActionGroup = { + id: FIRED_ACTIONS_ID, name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { defaultMessage: 'Fired', }), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 2d1df6e8cb462..48efe8fd45a3c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -5,10 +5,11 @@ */ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { AlertType } from '../../../../../alerts/server'; +import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server'; import { createInventoryMetricThresholdExecutor, FIRED_ACTIONS, + FIRED_ACTIONS_ID, } from './inventory_metric_threshold_executor'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; import { InfraBackendLibs } from '../../infra_types'; @@ -22,6 +23,7 @@ import { metricActionVariableDescription, thresholdActionVariableDescription, } from '../common/messages'; +import { RecoveredActionGroupId } from '../../../../../alerts/common'; const condition = schema.object({ threshold: schema.arrayOf(schema.number()), @@ -40,7 +42,21 @@ const condition = schema.object({ ), }); -export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs): AlertType => ({ +export type InventoryMetricThresholdAllowedActionGroups = typeof FIRED_ACTIONS_ID; + +export const registerMetricInventoryThresholdAlertType = ( + libs: InfraBackendLibs +): AlertType< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + InventoryMetricThresholdAllowedActionGroups, + RecoveredActionGroupId +> => ({ id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.inventory.alertName', { defaultMessage: 'Inventory', @@ -59,7 +75,7 @@ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs { unknowns: 'allow' } ), }, - defaultActionGroupId: FIRED_ACTIONS.id, + defaultActionGroupId: FIRED_ACTIONS_ID, actionGroups: [FIRED_ACTIONS], producer: 'infrastructure', minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index e04fe338f3436..dea808a29d1cb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -413,31 +413,6 @@ describe('Log threshold executor', () => { describe('Results processors', () => { describe('Can process ungrouped results', () => { - test('It handles the OK state correctly', () => { - const alertInstanceUpdaterMock = jest.fn(); - const alertParams = { - ...baseAlertParams, - criteria: [positiveCriteria[0]], - }; - const results = { - hits: { - total: { - value: 2, - }, - }, - } as UngroupedSearchQueryResponse; - processUngroupedResults( - results, - alertParams, - alertsMock.createAlertInstanceFactory, - alertInstanceUpdaterMock - ); - // First call, second argument - expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.OK); - // First call, third argument - expect(alertInstanceUpdaterMock.mock.calls[0][2]).toBe(undefined); - }); - test('It handles the ALERT state correctly', () => { const alertInstanceUpdaterMock = jest.fn(); const alertParams = { @@ -475,68 +450,6 @@ describe('Log threshold executor', () => { }); describe('Can process grouped results', () => { - test('It handles the OK state correctly', () => { - const alertInstanceUpdaterMock = jest.fn(); - const alertParams = { - ...baseAlertParams, - criteria: [positiveCriteria[0]], - groupBy: ['host.name', 'event.dataset'], - }; - const results = [ - { - key: { - 'host.name': 'i-am-a-host-name', - 'event.dataset': 'i-am-a-dataset', - }, - doc_count: 100, - filtered_results: { - doc_count: 1, - }, - }, - { - key: { - 'host.name': 'i-am-a-host-name', - 'event.dataset': 'i-am-a-dataset', - }, - doc_count: 100, - filtered_results: { - doc_count: 2, - }, - }, - { - key: { - 'host.name': 'i-am-a-host-name', - 'event.dataset': 'i-am-a-dataset', - }, - doc_count: 100, - filtered_results: { - doc_count: 3, - }, - }, - ] as GroupedSearchQueryResponse['aggregations']['groups']['buckets']; - processGroupByResults( - results, - alertParams, - alertsMock.createAlertInstanceFactory, - alertInstanceUpdaterMock - ); - expect(alertInstanceUpdaterMock.mock.calls.length).toBe(3); - // First call, second argument - expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.OK); - // First call, third argument - expect(alertInstanceUpdaterMock.mock.calls[0][2]).toBe(undefined); - - // Second call, second argument - expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.OK); - // Second call, third argument - expect(alertInstanceUpdaterMock.mock.calls[1][2]).toBe(undefined); - - // Third call, second argument - expect(alertInstanceUpdaterMock.mock.calls[2][1]).toBe(AlertStates.OK); - // Third call, third argument - expect(alertInstanceUpdaterMock.mock.calls[2][2]).toBe(undefined); - }); - test('It handles the ALERT state correctly', () => { const alertInstanceUpdaterMock = jest.fn(); const alertParams = { @@ -583,7 +496,7 @@ describe('Log threshold executor', () => { alertsMock.createAlertInstanceFactory, alertInstanceUpdaterMock ); - expect(alertInstanceUpdaterMock.mock.calls.length).toBe(results.length); + expect(alertInstanceUpdaterMock.mock.calls.length).toBe(2); // First call, second argument expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); // First call, third argument @@ -600,14 +513,9 @@ describe('Log threshold executor', () => { ]); // Second call, second argument - expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.OK); + expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.ALERT); // Second call, third argument - expect(alertInstanceUpdaterMock.mock.calls[1][2]).toBe(undefined); - - // Third call, second argument - expect(alertInstanceUpdaterMock.mock.calls[2][1]).toBe(AlertStates.ALERT); - // Third call, third argument - expect(alertInstanceUpdaterMock.mock.calls[2][2]).toEqual([ + expect(alertInstanceUpdaterMock.mock.calls[1][2]).toEqual([ { actionGroup: 'logs.threshold.fired', context: { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index d3d34cd2aad58..0044855a73f5c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -9,7 +9,12 @@ import { AlertExecutorOptions, AlertServices, AlertInstance, + AlertTypeParams, + AlertTypeState, AlertInstanceContext, + AlertInstanceState, + ActionGroup, + ActionGroupIdsOf, } from '../../../../../alerts/server'; import { AlertStates, @@ -20,12 +25,12 @@ import { UngroupedSearchQueryResponseRT, UngroupedSearchQueryResponse, GroupedSearchQueryResponse, - AlertParamsRT, + alertParamsRT, isRatioAlertParams, hasGroupBy, getNumerator, getDenominator, - Criteria, + CountCriteria, CountAlertParams, RatioAlertParams, } from '../../../../common/alerting/logs/log_threshold/types'; @@ -34,6 +39,20 @@ import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; +type LogThresholdActionGroups = ActionGroupIdsOf; +type LogThresholdAlertServices = AlertServices< + AlertInstanceState, + AlertInstanceContext, + LogThresholdActionGroups +>; +type LogThresholdAlertExecutorOptions = AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + LogThresholdActionGroups +>; + const COMPOSITE_GROUP_SIZE = 40; const checkValueAgainstComparatorMap: { @@ -46,17 +65,16 @@ const checkValueAgainstComparatorMap: { }; export const createLogThresholdExecutor = (libs: InfraBackendLibs) => - async function ({ services, params }: AlertExecutorOptions) { + async function ({ services, params }: LogThresholdAlertExecutorOptions) { const { alertInstanceFactory, savedObjectsClient, callCluster } = services; const { sources } = libs; const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const indexPattern = sourceConfiguration.configuration.logAlias; const timestampField = sourceConfiguration.configuration.fields.timestamp; - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); try { - const validatedParams = decodeOrThrow(AlertParamsRT)(params); + const validatedParams = decodeOrThrow(alertParamsRT)(params); if (!isRatioAlertParams(validatedParams)) { await executeAlert( @@ -76,10 +94,6 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => ); } } catch (e) { - alertInstance.replaceState({ - alertState: AlertStates.ERROR, - }); - throw new Error(e); } }; @@ -88,8 +102,8 @@ async function executeAlert( alertParams: CountAlertParams, timestampField: string, indexPattern: string, - callCluster: AlertServices['callCluster'], - alertInstanceFactory: AlertServices['alertInstanceFactory'] + callCluster: LogThresholdAlertServices['callCluster'], + alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] ) { const query = getESQuery(alertParams, timestampField, indexPattern); @@ -118,8 +132,8 @@ async function executeRatioAlert( alertParams: RatioAlertParams, timestampField: string, indexPattern: string, - callCluster: AlertServices['callCluster'], - alertInstanceFactory: AlertServices['alertInstanceFactory'] + callCluster: LogThresholdAlertServices['callCluster'], + alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] ) { // Ratio alert params are separated out into two standard sets of alert params const numeratorParams: AlertParams = { @@ -163,7 +177,7 @@ async function executeRatioAlert( } const getESQuery = ( - alertParams: Omit & { criteria: Criteria }, + alertParams: Omit & { criteria: CountCriteria }, timestampField: string, indexPattern: string ) => { @@ -175,15 +189,14 @@ const getESQuery = ( export const processUngroupedResults = ( results: UngroupedSearchQueryResponse, params: CountAlertParams, - alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; - - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); const documentCount = results.hits.total.value; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -195,8 +208,6 @@ export const processUngroupedResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }; @@ -204,17 +215,17 @@ export const processUngroupedRatioResults = ( numeratorResults: UngroupedSearchQueryResponse, denominatorResults: UngroupedSearchQueryResponse, params: RatioAlertParams, - alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); const numeratorCount = numeratorResults.hits.total.value; const denominatorCount = denominatorResults.hits.total.value; const ratio = getRatio(numeratorCount, denominatorCount); if (ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value)) { + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -227,8 +238,6 @@ export const processUngroupedRatioResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }; @@ -259,7 +268,7 @@ const getReducedGroupByResults = ( export const processGroupByResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: CountAlertParams, - alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -267,10 +276,10 @@ export const processGroupByResults = ( const groupResults = getReducedGroupByResults(results); groupResults.forEach((group) => { - const alertInstance = alertInstanceFactory(group.name); const documentCount = group.documentCount; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + const alertInstance = alertInstanceFactory(group.name); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -282,8 +291,6 @@ export const processGroupByResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }); }; @@ -292,7 +299,7 @@ export const processGroupByRatioResults = ( numeratorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], denominatorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: RatioAlertParams, - alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -301,7 +308,6 @@ export const processGroupByRatioResults = ( const denominatorGroupResults = getReducedGroupByResults(denominatorResults); numeratorGroupResults.forEach((numeratorGroup) => { - const alertInstance = alertInstanceFactory(numeratorGroup.name); const numeratorDocumentCount = numeratorGroup.documentCount; const denominatorGroup = denominatorGroupResults.find( (_group) => _group.name === numeratorGroup.name @@ -314,6 +320,7 @@ export const processGroupByRatioResults = ( ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value) ) { + const alertInstance = alertInstanceFactory(numeratorGroup.name); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -326,16 +333,14 @@ export const processGroupByRatioResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }); }; type AlertInstanceUpdater = ( - alertInstance: AlertInstance, + alertInstance: AlertInstance, state: AlertStates, - actions?: Array<{ actionGroup: string; context: AlertInstanceContext }> + actions?: Array<{ actionGroup: LogThresholdActionGroups; context: AlertInstanceContext }> ) => void; export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, actions) => { @@ -355,7 +360,7 @@ export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, }; export const buildFiltersFromCriteria = ( - params: Pick & { criteria: Criteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string ) => { const { timeSize, timeUnit, criteria } = params; @@ -406,7 +411,7 @@ export const buildFiltersFromCriteria = ( }; export const getGroupedESQuery = ( - params: Pick & { criteria: Criteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string, index: string ): object | undefined => { @@ -464,7 +469,7 @@ export const getGroupedESQuery = ( }; export const getUngroupedESQuery = ( - params: Pick & { criteria: Criteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string, index: string ): object => { @@ -498,7 +503,7 @@ type Filter = { [key in SupportedESQueryTypes]?: object; }; -const buildFiltersForCriteria = (criteria: Criteria) => { +const buildFiltersForCriteria = (criteria: CountCriteria) => { let filters: Filter[] = []; criteria.forEach((criterion) => { @@ -599,11 +604,17 @@ const getQueryMappingForComparator = (comparator: Comparator) => { return queryMappings[comparator]; }; -const getUngroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => { +const getUngroupedResults = async ( + query: object, + callCluster: LogThresholdAlertServices['callCluster'] +) => { return decodeOrThrow(UngroupedSearchQueryResponseRT)(await callCluster('search', query)); }; -const getGroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => { +const getGroupedResults = async ( + query: object, + callCluster: LogThresholdAlertServices['callCluster'] +) => { let compositeGroupBuckets: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] = []; let lastAfterKey: GroupedSearchQueryResponse['aggregations']['groups']['after_key'] | undefined; @@ -626,7 +637,7 @@ const getGroupedResults = async (query: object, callCluster: AlertServices['call return compositeGroupBuckets; }; -const createConditionsMessageForCriteria = (criteria: Criteria) => { +const createConditionsMessageForCriteria = (criteria: CountCriteria) => { const parts = criteria.map((criterion, index) => { const { field, comparator, value } = criterion; return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`; @@ -636,8 +647,9 @@ const createConditionsMessageForCriteria = (criteria: Criteria) => { // When the Alerting plugin implements support for multiple action groups, add additional // action groups here to send different messages, e.g. a recovery notification -export const FIRED_ACTIONS = { - id: 'logs.threshold.fired', +export const LogsThresholdFiredActionGroupId = 'logs.threshold.fired'; +export const FIRED_ACTIONS: ActionGroup<'logs.threshold.fired'> = { + id: LogsThresholdFiredActionGroupId, name: i18n.translate('xpack.infra.logs.alerting.threshold.fired', { defaultMessage: 'Fired', }), diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index 4703371f5e0de..236ab9b97fdc3 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { PluginSetupContract } from '../../../../../alerts/server'; +import { + PluginSetupContract, + AlertTypeParams, + AlertTypeState, + AlertInstanceContext, + AlertInstanceState, + ActionGroupIdsOf, +} from '../../../../../alerts/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - AlertParamsRT, + alertParamsRT, } from '../../../../common/alerting/logs/log_threshold/types'; import { InfraBackendLibs } from '../../infra_types'; import { decodeOrThrow } from '../../../../common/runtime_types'; @@ -79,14 +86,20 @@ export async function registerLogThresholdAlertType( ); } - alertingPlugin.registerType({ + alertingPlugin.registerType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + ActionGroupIdsOf + >({ id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.logs.alertName', { defaultMessage: 'Log threshold', }), validate: { params: { - validate: (params) => decodeOrThrow(AlertParamsRT)(params), + validate: (params) => decodeOrThrow(alertParamsRT)(params), }, }, defaultActionGroupId: FIRED_ACTIONS.id, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 49f82c7ccec0b..d51d9435fc904 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -12,7 +12,7 @@ import { import { InfraSource } from '../../../../../common/http_api/source_api'; import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler'; -import { AlertServices, AlertExecutorOptions } from '../../../../../../alerts/server'; +import { AlertServices } from '../../../../../../alerts/server'; import { getAllCompositeData } from '../../../../utils/get_all_composite_data'; import { DOCUMENT_COUNT_I18N } from '../../common/messages'; import { UNGROUPED_FACTORY_KEY } from '../../common/utils'; @@ -35,17 +35,19 @@ interface CompositeAggregationsResponse { }; } -export const evaluateAlert = ( +export interface EvaluatedAlertParams { + criteria: MetricExpressionParams[]; + groupBy: string | undefined | string[]; + filterQuery: string | undefined; +} + +export const evaluateAlert = ( callCluster: AlertServices['callCluster'], - params: AlertExecutorOptions['params'], + params: Params, config: InfraSource['configuration'], timeframe?: { start: number; end: number } ) => { - const { criteria, groupBy, filterQuery } = params as { - criteria: MetricExpressionParams[]; - groupBy: string | undefined | string[]; - filterQuery: string | undefined; - }; + const { criteria, groupBy, filterQuery } = params; return Promise.all( criteria.map(async (criterion) => { const currentValues = await getMetric( diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index a1d6428f3b52b..31561a8f6b145 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,14 +6,14 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; -import { RecoveredActionGroup } from '../../../../../alerts/common'; -import { AlertExecutorOptions } from '../../../../../alerts/server'; +// import { RecoveredActionGroup } from '../../../../../alerts/common'; import { alertsMock, AlertServicesMock, AlertInstanceMock, } from '../../../../../alerts/server/mocks'; import { InfraSources } from '../../sources'; +import { MetricThresholdAlertExecutorOptions } from './register_metric_threshold_alert_type'; interface AlertTestInstance { instance: AlertInstanceMock; @@ -21,13 +21,25 @@ interface AlertTestInstance { state: any; } -let persistAlertInstances = false; +let persistAlertInstances = false; // eslint-disable-line prefer-const + +const mockOptions = { + alertId: '', + startedAt: new Date(), + previousStartedAt: null, + state: {}, + spaceId: '', + name: '', + tags: [], + createdBy: null, + updatedBy: null, +}; describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => - executor({ + executor(({ services, params: { sourceId, @@ -39,7 +51,10 @@ describe('The metric threshold alert type', () => { }, ], }, - }); + /** + * TODO: Remove this use of `as` by utilizing a proper type + */ + } as unknown) as MetricThresholdAlertExecutorOptions); test('alerts as expected with the > comparator', async () => { await execute(Comparator.GT, [0.75]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); @@ -109,6 +124,7 @@ describe('The metric threshold alert type', () => { describe('querying with a groupBy parameter', () => { const execute = (comparator: Comparator, threshold: number[]) => executor({ + ...mockOptions, services, params: { groupBy: 'something', @@ -159,6 +175,7 @@ describe('The metric threshold alert type', () => { groupBy: string = '' ) => executor({ + ...mockOptions, services, params: { groupBy, @@ -216,6 +233,7 @@ describe('The metric threshold alert type', () => { const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[]) => executor({ + ...mockOptions, services, params: { criteria: [ @@ -242,6 +260,7 @@ describe('The metric threshold alert type', () => { const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[]) => executor({ + ...mockOptions, services, params: { criteria: [ @@ -268,6 +287,7 @@ describe('The metric threshold alert type', () => { const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[]) => executor({ + ...mockOptions, services, params: { criteria: [ @@ -294,6 +314,7 @@ describe('The metric threshold alert type', () => { const instanceID = '*'; const execute = (alertOnNoData: boolean) => executor({ + ...mockOptions, services, params: { criteria: [ @@ -323,6 +344,7 @@ describe('The metric threshold alert type', () => { const instanceID = '*'; const execute = () => executor({ + ...mockOptions, services, params: { criteria: [ @@ -344,10 +366,18 @@ describe('The metric threshold alert type', () => { }); }); + /* + * Custom recovery actions aren't yet available in the alerting framework + * Uncomment the code below once they've been implemented + * Reference: https://github.com/elastic/kibana/issues/87048 + */ + + /* describe('querying a metric that later recovers', () => { const instanceID = '*'; const execute = (threshold: number[]) => executor({ + ...mockOptions, services, params: { criteria: [ @@ -387,11 +417,13 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); + */ describe('querying a metric with a percentage metric', () => { const instanceID = '*'; const execute = () => executor({ + ...mockOptions, services, params: { sourceId: 'default', @@ -435,10 +467,7 @@ const mockLibs: any = { configuration: createMockStaticConfiguration({}), }; -const executor = createMetricThresholdExecutor(mockLibs) as (opts: { - params: AlertExecutorOptions['params']; - services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; -}) => Promise; +const executor = createMetricThresholdExecutor(mockLibs); const services: AlertServicesMock = alertsMock.createAlertServices(); services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 60790648d9a9b..1b8e018659ee5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -7,21 +7,26 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { RecoveredActionGroup } from '../../../../../alerts/common'; -import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InfraBackendLibs } from '../../infra_types'; import { buildErrorAlertReason, buildFiredAlertReason, buildNoDataAlertReason, - buildRecoveredAlertReason, + // buildRecoveredAlertReason, stateToAlertMessage, } from '../common/messages'; import { createFormatter } from '../../../../common/formatters'; import { AlertStates } from './types'; -import { evaluateAlert } from './lib/evaluate_alert'; +import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; +import { + MetricThresholdAlertExecutorOptions, + MetricThresholdAlertType, +} from './register_metric_threshold_alert_type'; -export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => - async function (options: AlertExecutorOptions) { +export const createMetricThresholdExecutor = ( + libs: InfraBackendLibs +): MetricThresholdAlertType['executor'] => + async function (options: MetricThresholdAlertExecutorOptions) { const { services, params } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); @@ -36,7 +41,11 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => sourceId || 'default' ); const config = source.configuration; - const alertResults = await evaluateAlert(services.callCluster, params, config); + const alertResults = await evaluateAlert( + services.callCluster, + params as EvaluatedAlertParams, + config + ); // Because each alert result has the same group definitions, just grab the groups from the first one. const groups = Object.keys(first(alertResults)!); @@ -68,9 +77,14 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => .map((result) => buildFiredAlertReason(formatAlertResult(result[group]))) .join('\n'); } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { - reason = alertResults - .map((result) => buildRecoveredAlertReason(formatAlertResult(result[group]))) - .join('\n'); + /* + * Custom recovery actions aren't yet available in the alerting framework + * Uncomment the code below once they've been implemented + * Reference: https://github.com/elastic/kibana/issues/87048 + */ + // reason = alertResults + // .map((result) => buildRecoveredAlertReason(formatAlertResult(result[group]))) + // .join('\n'); } if (alertOnNoData) { if (nextState === AlertStates.NO_DATA) { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index f04a1015bcbcd..77126e7d9454c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -5,7 +5,13 @@ */ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { AlertType } from '../../../../../alerts/server'; +import { + AlertType, + AlertInstanceState, + AlertInstanceContext, + AlertExecutorOptions, + ActionGroupIdsOf, +} from '../../../../../alerts/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; @@ -21,7 +27,28 @@ import { thresholdActionVariableDescription, } from '../common/messages'; -export function registerMetricThresholdAlertType(libs: InfraBackendLibs): AlertType { +export type MetricThresholdAlertType = AlertType< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + ActionGroupIdsOf +>; +export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + ActionGroupIdsOf +>; + +export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType { const baseCriterion = { threshold: schema.arrayOf(schema.number()), comparator: oneOfLiterals(Object.values(Comparator)), diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 1496b0c335322..5e38cb49114e9 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -74,6 +74,16 @@ function createMockFrame(): jest.Mocked { }; } +function createMockSearchService() { + let sessionIdCounter = 1; + return { + session: { + start: jest.fn(() => `sessionId-${sessionIdCounter++}`), + clear: jest.fn(), + }, + }; +} + function createMockFilterManager() { const unsubscribe = jest.fn(); @@ -118,16 +128,29 @@ function createMockQueryString() { function createMockTimefilter() { const unsubscribe = jest.fn(); + let timeFilter = { from: 'now-7d', to: 'now' }; + let subscriber: () => void; return { - getTime: jest.fn(() => ({ from: 'now-7d', to: 'now' })), - setTime: jest.fn(), + getTime: jest.fn(() => timeFilter), + setTime: jest.fn((newTimeFilter) => { + timeFilter = newTimeFilter; + if (subscriber) { + subscriber(); + } + }), getTimeUpdate$: () => ({ subscribe: ({ next }: { next: () => void }) => { + subscriber = next; return unsubscribe; }, }), getRefreshInterval: () => {}, getRefreshIntervalDefaults: () => {}, + getAutoRefreshFetch$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + return next; + }, + }), }; } @@ -209,6 +232,7 @@ describe('Lens App', () => { return new Promise((resolve) => resolve({ id })); }), }, + search: createMockSearchService(), } as unknown) as DataPublicPluginStart, storage: { get: jest.fn(), @@ -295,6 +319,7 @@ describe('Lens App', () => { "query": "", }, "savedQuery": undefined, + "searchSessionId": "sessionId-1", "showNoDataPopover": [Function], }, ], @@ -1072,6 +1097,53 @@ describe('Lens App', () => { }) ); }); + + it('updates the searchSessionId when the user changes query or time in the search bar', () => { + const { component, frame, services } = mountWith({}); + act(() => + component.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: '', language: 'lucene' }, + }) + ); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + searchSessionId: `sessionId-1`, + }) + ); + + // trigger again, this time changing just the query + act(() => + component.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + searchSessionId: `sessionId-2`, + }) + ); + + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + act(() => + services.data.query.filterManager.setFilters([ + esFilters.buildExistsFilter(field, indexPattern), + ]) + ); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + searchSessionId: `sessionId-3`, + }) + ); + }); }); describe('saved query handling', () => { @@ -1165,6 +1237,37 @@ describe('Lens App', () => { ); }); + it('updates the searchSessionId when the query is updated', () => { + const { component, frame } = mountWith({}); + act(() => { + component.find(TopNavMenu).prop('onSaved')!({ + id: '1', + attributes: { + title: '', + description: '', + query: { query: '', language: 'lucene' }, + }, + }); + }); + act(() => { + component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + id: '2', + attributes: { + title: 'new title', + description: '', + query: { query: '', language: 'lucene' }, + }, + }); + }); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + searchSessionId: `sessionId-1`, + }) + ); + }); + it('clears all existing unpinned filters when the active saved query is cleared', () => { const { component, frame, services } = mountWith({}); act(() => @@ -1190,6 +1293,32 @@ describe('Lens App', () => { }) ); }); + + it('updates the searchSessionId when the active saved query is cleared', () => { + const { component, frame, services } = mountWith({}); + act(() => + component.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const unpinned = esFilters.buildExistsFilter(field, indexPattern); + const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); + component.update(); + act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + searchSessionId: `sessionId-2`, + }) + ); + }); }); describe('showing a confirm message when leaving', () => { diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index bb77c5998519d..3f10cb341105c 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -7,7 +7,7 @@ import './app.scss'; import _ from 'lodash'; -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; import { EuiBreadcrumb } from '@elastic/eui'; @@ -71,7 +71,6 @@ export function App({ } = useKibana().services; const [state, setState] = useState(() => { - const currentRange = data.query.timefilter.timefilter.getTime(); return { query: data.query.queryString.getQuery(), // Do not use app-specific filters from previous app, @@ -81,14 +80,11 @@ export function App({ : data.query.filterManager.getFilters(), isLoading: Boolean(initialInput), indexPatternsForTopNav: [], - dateRange: { - fromDate: currentRange.from, - toDate: currentRange.to, - }, isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp), isSaveModalVisible: false, indicateNoData: false, isSaveable: false, + searchSessionId: data.search.session.start(), }; }); @@ -107,10 +103,14 @@ export function App({ state.indicateNoData, state.query, state.filters, - state.dateRange, state.indexPatternsForTopNav, + state.searchSessionId, ]); + // Need a stable reference for the frame component of the dateRange + const { from: fromDate, to: toDate } = data.query.timefilter.timefilter.getTime(); + const currentDateRange = useMemo(() => ({ fromDate, toDate }), [fromDate, toDate]); + const onError = useCallback( (e: { message: string }) => notifications.toasts.addDanger({ @@ -160,24 +160,35 @@ export function App({ const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ next: () => { - setState((s) => ({ ...s, filters: data.query.filterManager.getFilters() })); + setState((s) => ({ + ...s, + filters: data.query.filterManager.getFilters(), + searchSessionId: data.search.session.start(), + })); trackUiEvent('app_filters_updated'); }, }); const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ next: () => { - const currentRange = data.query.timefilter.timefilter.getTime(); setState((s) => ({ ...s, - dateRange: { - fromDate: currentRange.from, - toDate: currentRange.to, - }, + searchSessionId: data.search.session.start(), })); }, }); + const autoRefreshSubscription = data.query.timefilter.timefilter + .getAutoRefreshFetch$() + .subscribe({ + next: () => { + setState((s) => ({ + ...s, + searchSessionId: data.search.session.start(), + })); + }, + }); + const kbnUrlStateStorage = createKbnUrlStateStorage({ history, useHash: uiSettings.get('state:storeInSessionStorage'), @@ -192,10 +203,12 @@ export function App({ stopSyncingQueryServiceStateWithUrl(); filterSubscription.unsubscribe(); timeSubscription.unsubscribe(); + autoRefreshSubscription.unsubscribe(); }; }, [ data.query.filterManager, data.query.timefilter.timefilter, + data.search.session, notifications.toasts, uiSettings, data.query, @@ -594,21 +607,21 @@ export function App({ appName={'lens'} onQuerySubmit={(payload) => { const { dateRange, query } = payload; - if ( - dateRange.from !== state.dateRange.fromDate || - dateRange.to !== state.dateRange.toDate - ) { + const currentRange = data.query.timefilter.timefilter.getTime(); + if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) { data.query.timefilter.timefilter.setTime(dateRange); trackUiEvent('app_date_change'); } else { + // Query has changed, renew the session id. + // Time change will be picked up by the time subscription + setState((s) => ({ + ...s, + searchSessionId: data.search.session.start(), + })); trackUiEvent('app_query_change'); } setState((s) => ({ ...s, - dateRange: { - fromDate: dateRange.from, - toDate: dateRange.to, - }, query: query || s.query, })); }} @@ -622,12 +635,6 @@ export function App({ setState((s) => ({ ...s, savedQuery: { ...savedQuery }, // Shallow query for reference issues - dateRange: savedQuery.attributes.timefilter - ? { - fromDate: savedQuery.attributes.timefilter.from, - toDate: savedQuery.attributes.timefilter.to, - } - : s.dateRange, })); }} onClearSavedQuery={() => { @@ -640,8 +647,8 @@ export function App({ })); }} query={state.query} - dateRangeFrom={state.dateRange.fromDate} - dateRangeTo={state.dateRange.toDate} + dateRangeFrom={fromDate} + dateRangeTo={toDate} indicateNoData={state.indicateNoData} />
    @@ -650,7 +657,8 @@ export function App({ className="lnsApp__frame" render={editorFrame.mount} nativeProps={{ - dateRange: state.dateRange, + searchSessionId: state.searchSessionId, + dateRange: currentDateRange, query: state.query, filters: state.filters, savedQuery: state.savedQuery, diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 2c23dc291405c..ad354510ef049 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -46,7 +46,7 @@ export function getLensTopNavConfig(options: { if (showCancel) { topNavMenu.push({ label: i18n.translate('xpack.lens.app.cancel', { - defaultMessage: 'cancel', + defaultMessage: 'Cancel', }), run: actions.cancel, testId: 'lnsApp_cancelButton', diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index fbfd9c5758948..e769e402ff0e1 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -216,6 +216,7 @@ export async function mountApp( params.element ); return () => { + data.search.session.clear(); instance.unmount(); unmountComponentAtNode(params.element); unlistenParentHistory(); diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 869ccf52fb0bd..af0feabe68cf7 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -55,16 +55,12 @@ export interface LensAppState { // Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb. isLinkedToOriginatingApp?: boolean; - // Properties needed to interface with TopNav - dateRange: { - fromDate: string; - toDate: string; - }; query: Query; filters: Filter[]; savedQuery?: SavedQuery; isSaveable: boolean; activeData?: TableInspectorAdapter; + searchSessionId: string; } export interface RedirectToOriginProps { diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 4d1df5b519ba9..57289fc0ac169 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -22,7 +22,7 @@ import { EuiBasicTableColumn, EuiTableActionsColumnType, } from '@elastic/eui'; -import { orderBy } from 'lodash'; + import { IAggType } from 'src/plugins/data/public'; import { Datatable, DatatableColumnMeta, RenderMode } from 'src/plugins/expressions'; import { @@ -41,6 +41,7 @@ import { VisualizationContainer } from '../visualization_container'; import { EmptyPlaceholder } from '../shared_components'; import { desanitizeFilterContext } from '../utils'; import { LensIconChartDatatable } from '../assets/chart_datatable'; +import { getSortingCriteria } from './sorting'; export const LENS_EDIT_SORT_ACTION = 'sort'; @@ -92,6 +93,10 @@ export interface DatatableRender { value: DatatableProps; } +function isRange(meta: { params?: { id?: string } } | undefined) { + return meta?.params?.id === 'range'; +} + export const getDatatable = ({ formatFactory, }: { @@ -139,17 +144,18 @@ export const getDatatable = ({ if (sortBy && sortDirection !== 'none') { // Sort on raw values for these types, while use the formatted value for the rest - const sortingCriteria = ['number', 'date'].includes( - columnsReverseLookup[sortBy]?.meta?.type || '' - ) - ? sortBy - : (row: Record) => formatters[sortBy]?.convert(row[sortBy]); - // replace the table here - context.inspectorAdapters.tables[layerId].rows = orderBy( - firstTable.rows || [], - [sortingCriteria], - sortDirection as Direction + const sortingCriteria = getSortingCriteria( + isRange(columnsReverseLookup[sortBy]?.meta) + ? 'range' + : columnsReverseLookup[sortBy]?.meta?.type, + sortBy, + formatters[sortBy], + sortDirection ); + // replace the table here + context.inspectorAdapters.tables[layerId].rows = (firstTable.rows || []) + .slice() + .sort(sortingCriteria); // replace also the local copy firstTable.rows = context.inspectorAdapters.tables[layerId].rows; } diff --git a/x-pack/plugins/lens/public/datatable_visualization/sorting.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/sorting.test.tsx new file mode 100644 index 0000000000000..bd8678455c63c --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/sorting.test.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSortingCriteria } from './sorting'; +import { FieldFormat } from 'src/plugins/data/public'; +import { DatatableColumnType } from 'src/plugins/expressions'; + +function getMockFormatter() { + return { convert: (v: unknown) => `${v as string}` } as FieldFormat; +} + +function testSorting({ + input, + output, + direction, + type, + keepLast, +}: { + input: unknown[]; + output: unknown[]; + direction: 'asc' | 'desc'; + type: DatatableColumnType | 'range'; + keepLast?: boolean; // special flag to handle values that should always be last no matter the direction +}) { + const datatable = input.map((v) => ({ + a: v, + })); + const sorted = output.map((v) => ({ a: v })); + if (direction === 'desc') { + sorted.reverse(); + if (keepLast) { + // Cycle shift of the first element + const firstEl = sorted.shift()!; + sorted.push(firstEl); + } + } + const criteria = getSortingCriteria(type, 'a', getMockFormatter(), direction); + expect(datatable.sort(criteria)).toEqual(sorted); +} + +describe('Data sorting criteria', () => { + describe('Numeric values', () => { + for (const direction of ['asc', 'desc'] as const) { + it(`should provide the number criteria of numeric values (${direction})`, () => { + testSorting({ + input: [7, 6, 5, -Infinity, Infinity], + output: [-Infinity, 5, 6, 7, Infinity], + direction, + type: 'number', + }); + }); + + it(`should provide the number criteria for date values (${direction})`, () => { + const now = Date.now(); + testSorting({ + input: [now, 0, now - 150000], + output: [0, now - 150000, now], + direction, + type: 'date', + }); + }); + } + }); + + describe('String or anything else as string', () => { + for (const direction of ['asc', 'desc'] as const) { + it(`should provide the string criteria for terms values (${direction})`, () => { + testSorting({ + input: ['a', 'b', 'c', 'd', '12'], + output: ['12', 'a', 'b', 'c', 'd'], + direction, + type: 'string', + }); + }); + + it(`should provide the string criteria for other types of values (${direction})`, () => { + testSorting({ + input: [true, false, false], + output: [false, false, true], + direction, + type: 'boolean', + }); + }); + } + }); + + describe('IP sorting', () => { + for (const direction of ['asc', 'desc'] as const) { + it(`should provide the IP criteria for IP values (IPv4 only values) - ${direction}`, () => { + testSorting({ + input: ['127.0.0.1', '192.168.1.50', '200.100.100.10', '10.0.1.76', '8.8.8.8'], + output: ['8.8.8.8', '10.0.1.76', '127.0.0.1', '192.168.1.50', '200.100.100.10'], + direction, + type: 'ip', + }); + }); + + it(`should provide the IP criteria for IP values (IPv6 only values) - ${direction}`, () => { + testSorting({ + input: [ + 'fc00::123', + '::1', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + '2001:db8:1234:0000:0000:0000:0000:0000', + '2001:db8:1234::', // equivalent to the above + ], + output: [ + '::1', + '2001:db8:1234::', + '2001:db8:1234:0000:0000:0000:0000:0000', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + 'fc00::123', + ], + direction, + type: 'ip', + }); + }); + + it(`should provide the IP criteria for IP values (mixed values) - ${direction}`, () => { + // A mix of IPv4, IPv6, IPv4 mapped to IPv6 + testSorting({ + input: [ + 'fc00::123', + '192.168.1.50', + '::FFFF:192.168.1.50', // equivalent to the above with the IPv6 mapping + '10.0.1.76', + '8.8.8.8', + '::1', + ], + output: [ + '::1', + '8.8.8.8', + '10.0.1.76', + '192.168.1.50', + '::FFFF:192.168.1.50', + 'fc00::123', + ], + direction, + type: 'ip', + }); + }); + + it(`should provide the IP criteria for IP values (mixed values with invalid "Other" field) - ${direction}`, () => { + testSorting({ + input: ['fc00::123', '192.168.1.50', 'Other', '10.0.1.76', '8.8.8.8', '::1'], + output: ['::1', '8.8.8.8', '10.0.1.76', '192.168.1.50', 'fc00::123', 'Other'], + direction, + type: 'ip', + keepLast: true, + }); + }); + } + }); + + describe('Range sorting', () => { + for (const direction of ['asc', 'desc'] as const) { + it(`should sort closed ranges - ${direction}`, () => { + testSorting({ + input: [ + { gte: 1, lt: 5 }, + { gte: 0, lt: 5 }, + { gte: 0, lt: 1 }, + ], + output: [ + { gte: 0, lt: 1 }, + { gte: 0, lt: 5 }, + { gte: 1, lt: 5 }, + ], + direction, + type: 'range', + }); + }); + + it(`should sort open ranges - ${direction}`, () => { + testSorting({ + input: [{ gte: 1, lt: 5 }, { gte: 0, lt: 5 }, { gte: 0 }], + output: [{ gte: 0, lt: 5 }, { gte: 0 }, { gte: 1, lt: 5 }], + direction, + type: 'range', + }); + }); + } + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/sorting.tsx b/x-pack/plugins/lens/public/datatable_visualization/sorting.tsx new file mode 100644 index 0000000000000..89def8fe90aea --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/sorting.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ipaddr from 'ipaddr.js'; +import type { IPv4, IPv6 } from 'ipaddr.js'; +import { FieldFormat } from 'src/plugins/data/public'; + +function isIPv6Address(ip: IPv4 | IPv6): ip is IPv6 { + return ip.kind() === 'ipv6'; +} + +function getSafeIpAddress(ip: string, directionFactor: number) { + if (!ipaddr.isValid(ip)) { + // for non valid IPs have the same behaviour as for now (we assume it's only the "Other" string) + // create a mock object which has all a special value to keep them always at the bottom of the list + return { parts: Array(8).fill(directionFactor * Infinity) }; + } + const parsedIp = ipaddr.parse(ip); + return isIPv6Address(parsedIp) ? parsedIp : parsedIp.toIPv4MappedAddress(); +} + +function getIPCriteria(sortBy: string, directionFactor: number) { + // Create a set of 8 function to sort based on the 8 IPv6 slots of an address + // For IPv4 bring them to the IPv6 "mapped" format and then sort + return (rowA: Record, rowB: Record) => { + const ipAString = rowA[sortBy] as string; + const ipBString = rowB[sortBy] as string; + const ipA = getSafeIpAddress(ipAString, directionFactor); + const ipB = getSafeIpAddress(ipBString, directionFactor); + + // Now compare each part of the IPv6 address and exit when a value != 0 is found + let i = 0; + let diff = ipA.parts[i] - ipB.parts[i]; + while (!diff && i < 7) { + i++; + diff = ipA.parts[i] - ipB.parts[i]; + } + + // in case of same address but written in different styles, sort by string length + if (diff === 0) { + return directionFactor * (ipAString.length - ipBString.length); + } + return directionFactor * diff; + }; +} + +function getRangeCriteria(sortBy: string, directionFactor: number) { + // fill missing fields with these open bounds to perform number sorting + const openRange = { gte: -Infinity, lt: Infinity }; + return (rowA: Record, rowB: Record) => { + const rangeA = { ...openRange, ...(rowA[sortBy] as Omit) }; + const rangeB = { ...openRange, ...(rowB[sortBy] as Omit) }; + + const fromComparison = rangeA.gte - rangeB.gte; + const toComparison = rangeA.lt - rangeB.lt; + + return directionFactor * (fromComparison || toComparison); + }; +} + +export function getSortingCriteria( + type: string | undefined, + sortBy: string, + formatter: FieldFormat, + direction: string +) { + // handle the direction with a multiply factor. + const directionFactor = direction === 'asc' ? 1 : -1; + + if (['number', 'date'].includes(type || '')) { + return (rowA: Record, rowB: Record) => + directionFactor * ((rowA[sortBy] as number) - (rowB[sortBy] as number)); + } + // this is a custom type, and can safely assume the gte and lt fields are all numbers or undefined + if (type === 'range') { + return getRangeCriteria(sortBy, directionFactor); + } + // IP have a special sorting + if (type === 'ip') { + return getIPCriteria(sortBy, directionFactor); + } + // use a string sorter for the rest + return (rowA: Record, rowB: Record) => { + const aString = formatter.convert(rowA[sortBy]); + const bString = formatter.convert(rowB[sortBy]); + return directionFactor * aString.localeCompare(bString); + }; +} diff --git a/x-pack/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 index b0879ac8cb886..ef95314c55581 100644 --- a/x-pack/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 @@ -60,6 +60,7 @@ function getDefaultProps() { }, palettes: chartPluginMock.createPaletteRegistry(), showNoDataPopover: jest.fn(), + searchSessionId: 'sessionId', }; } @@ -264,6 +265,7 @@ describe('editor_frame', () => { filters: [], dateRange: { fromDate: 'now-7d', toDate: 'now' }, availablePalettes: defaultProps.palettes, + searchSessionId: 'sessionId', }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 977947b5afbeb..d872920d815ad 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -43,6 +43,7 @@ export interface EditorFrameProps { query: Query; filters: Filter[]; savedQuery?: SavedQuery; + searchSessionId: string; onChange: (arg: { filterableIndexPatterns: string[]; doc: Document; @@ -105,7 +106,7 @@ export function EditorFrame(props: EditorFrameProps) { dateRange: props.dateRange, query: props.query, filters: props.filters, - + searchSessionId: props.searchSessionId, availablePalettes: props.palettes, addNewLayer() { diff --git a/x-pack/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 index 792fdc6d1ace7..52328bc3a1440 100644 --- a/x-pack/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 @@ -39,6 +39,7 @@ describe('editor_frame state management', () => { query: { query: '', language: 'lucene' }, filters: [], showNoDataPopover: jest.fn(), + searchSessionId: 'sessionId', }; }); diff --git a/x-pack/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 index 338a998b6b4dc..e2c4fa959924a 100644 --- a/x-pack/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 @@ -273,16 +273,18 @@ export function SuggestionPanel({ const contextRef = useRef(context); contextRef.current = context; + const sessionIdRef = useRef(frame.searchSessionId); + sessionIdRef.current = frame.searchSessionId; + const AutoRefreshExpressionRenderer = useMemo(() => { - const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$(); return (props: ReactExpressionRendererProps) => ( ); - }, [plugins.data.query.timefilter.timefilter, ExpressionRendererComponent]); + }, [ExpressionRendererComponent]); const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-1); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index deb48027512cc..6411b0e5f1ad9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -34,7 +34,7 @@ import { IFieldType, IIndexPattern, } from '../../../../../../../src/plugins/data/public'; -import { TriggerId, UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; +import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; @@ -48,12 +48,12 @@ describe('workspace_panel', () => { let expressionRendererMock: jest.Mock; let uiActionsMock: jest.Mocked; let dataMock: jest.Mocked; - let trigger: jest.Mocked>; + let trigger: jest.Mocked; let instance: ReactWrapper; beforeEach(() => { - trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked>; + trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked; uiActionsMock = uiActionsPluginMock.createStartContract(); dataMock = dataPluginMock.createStartContract(); uiActionsMock.getTrigger.mockReturnValue(trigger); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 8820f26479cf9..eb16dabfd2f90 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -162,7 +162,7 @@ export function WorkspacePanel({ const expression = useMemo( () => { - if (!configurationValidationError || configurationValidationError.length === 0) { + if (!configurationValidationError?.length) { try { return buildExpression({ visualization: activeVisualization, @@ -362,8 +362,6 @@ export const InnerVisualizationWrapper = ({ }; ExpressionRendererComponent: ReactExpressionRendererType; }) => { - const autoRefreshFetch$ = useMemo(() => timefilter.getAutoRefreshFetch$(), [timefilter]); - const context: ExecutionContextSearch = useMemo( () => ({ query: framePublicAPI.query, @@ -400,13 +398,17 @@ export const InnerVisualizationWrapper = ({ showExtraErrors = localState.configurationValidationError .slice(1) .map(({ longMessage }) => ( - + {longMessage} )); } else { showExtraErrors = ( - + { setLocalState((prevState: WorkspaceState) => ({ @@ -414,6 +416,7 @@ export const InnerVisualizationWrapper = ({ expandError: !prevState.expandError, })); }} + data-test-subj="configuration-failure-more-errors" > {i18n.translate('xpack.lens.editorFrame.configurationFailureMoreErrors', { defaultMessage: ` +{errors} {errors, plural, one {error} other {errors}}`, @@ -445,7 +448,7 @@ export const InnerVisualizationWrapper = ({ - + {localState.configurationValidationError[0].longMessage} {showExtraErrors} @@ -477,7 +480,7 @@ export const InnerVisualizationWrapper = ({ padding="m" expression={expression!} searchContext={context} - reload$={autoRefreshFetch$} + searchSessionId={framePublicAPI.searchSessionId} onEvent={onEvent} onData$={onData$} renderMode="edit" diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index b00760e9664f3..ea7ce99e92cef 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -260,6 +260,7 @@ export class Embeddable handleEvent={this.handleEvent} onData$={this.updateActiveData} renderMode={input.renderMode} + syncColors={input.syncColors} hasCompatibleActions={this.hasCompatibleActions} />, domNode diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index e2607886a4219..c91ca74b54a4f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -29,6 +29,7 @@ export interface ExpressionWrapperProps { inspectorAdapters?: Partial | undefined ) => void; renderMode?: RenderMode; + syncColors?: boolean; hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions']; } @@ -41,6 +42,7 @@ export function ExpressionWrapper({ searchSessionId, onData$, renderMode, + syncColors, hasCompatibleActions, }: ExpressionWrapperProps) { return ( @@ -70,6 +72,7 @@ export function ExpressionWrapper({ searchSessionId={searchSessionId} onData$={onData$} renderMode={renderMode} + syncColors={syncColors} renderError={(errorMessage, error) => (
    diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 5ab410a1c0af2..2152c18ffeda4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -132,6 +132,7 @@ export function createMockFramePublicAPI(): FrameMock { get: () => palette, getAll: () => [palette], }, + searchSessionId: 'sessionId', }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx index e9f8013ef7e2d..3687e0cce2f1d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx @@ -57,6 +57,7 @@ describe('editor_frame service', () => { indexPatternId: '1', fieldName: 'test', }, + searchSessionId: 'sessionId', }); instance.unmount(); })() @@ -78,6 +79,7 @@ describe('editor_frame service', () => { query: { query: '', language: 'lucene' }, filters: [], showNoDataPopover: jest.fn(), + searchSessionId: 'sessionId', }); instance.unmount(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 0562e9bf4d32e..d4e9522f3bed1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -138,6 +138,7 @@ export class EditorFrameService { onChange, showNoDataPopover, initialContext, + searchSessionId, } ) => { domElement = element; @@ -172,6 +173,7 @@ export class EditorFrameService { onChange={onChange} showNoDataPopover={showNoDataPopover} initialContext={initialContext} + searchSessionId={searchSessionId} /> , domElement diff --git a/x-pack/plugins/lens/public/help_menu_util.tsx b/x-pack/plugins/lens/public/help_menu_util.tsx index 333a90df4731b..6169ca7bddc50 100644 --- a/x-pack/plugins/lens/public/help_menu_util.tsx +++ b/x-pack/plugins/lens/public/help_menu_util.tsx @@ -12,7 +12,7 @@ export function addHelpMenuToAppChrome(chrome: ChromeStart, docLinks: DocLinksSt links: [ { linkType: 'documentation', - href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/lens.html`, + href: docLinks.links.visualize.lensPanels, }, { linkType: 'github', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index c655fc18ab5fa..1144a1043c5b1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -16,6 +16,7 @@ import { EuiListGroupItemProps, EuiFormLabel, EuiToolTip, + EuiText, } from '@elastic/eui'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { OperationSupportMatrix } from './operation_support'; @@ -29,6 +30,7 @@ import { updateColumnParam, resetIncomplete, FieldBasedIndexPatternColumn, + canTransition, } from '../operations'; import { mergeLayer } from '../state_helpers'; import { FieldSelect } from './field_select'; @@ -37,6 +39,7 @@ import { BucketNestingEditor } from './bucket_nesting_editor'; import { IndexPattern, IndexPatternLayer } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { FormatSelector } from './format_selector'; +import { ReferenceEditor } from './reference_editor'; import { TimeScaling } from './time_scaling'; const operationPanels = getOperationDisplay(); @@ -145,18 +148,26 @@ export function DimensionEditor(props: DimensionEditorProps) { const operationsWithCompatibility = [...possibleOperations].map((operationType) => { const definition = operationDefinitionMap[operationType]; + const currentField = + selectedColumn && + hasField(selectedColumn) && + currentIndexPattern.getFieldByName(selectedColumn.sourceField); return { operationType, - compatibleWithCurrentField: - !selectedColumn || - (selectedColumn && - hasField(selectedColumn) && - definition.input === 'field' && - fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || - (selectedColumn && !hasField(selectedColumn) && definition.input === 'none'), + compatibleWithCurrentField: canTransition({ + layer: state.layers[layerId], + columnId, + op: operationType, + indexPattern: currentIndexPattern, + field: currentField || undefined, + filterOperations: props.filterOperations, + }), disabledStatus: definition.getDisabledStatus && - definition.getDisabledStatus(state.indexPatterns[state.currentIndexPatternId]), + definition.getDisabledStatus( + state.indexPatterns[state.currentIndexPatternId], + state.layers[layerId] + ), }; }); @@ -180,7 +191,15 @@ export function DimensionEditor(props: DimensionEditorProps) { } let label: EuiListGroupItemProps['label'] = operationPanels[operationType].displayName; - if (disabledStatus) { + if (isActive && disabledStatus) { + label = ( + + + {operationPanels[operationType].displayName} + + + ); + } else if (disabledStatus) { label = ( {operationPanels[operationType].displayName} @@ -202,9 +221,12 @@ export function DimensionEditor(props: DimensionEditorProps) { compatibleWithCurrentField ? '' : ' incompatible' }`, onClick() { - if (operationDefinitionMap[operationType].input === 'none') { + if ( + operationDefinitionMap[operationType].input === 'none' || + operationDefinitionMap[operationType].input === 'fullReference' + ) { + // Clear invalid state because we are reseting to a valid column if (selectedColumn?.operationType === operationType) { - // Clear invalid state because we are reseting to a valid column if (incompleteInfo) { setStateWrapper(resetIncomplete(state.layers[layerId], columnId)); } @@ -291,6 +313,35 @@ export function DimensionEditor(props: DimensionEditorProps) {
    + {!incompleteInfo && + selectedColumn && + 'references' in selectedColumn && + selectedOperationDefinition?.input === 'fullReference' ? ( + <> + {selectedColumn.references.map((referenceId, index) => { + const validation = selectedOperationDefinition.requiredReferences[index]; + + return ( + { + setState(mergeLayer({ state, layerId, newLayer })); + }} + validation={validation} + currentIndexPattern={currentIndexPattern} + existingFields={state.existingFields} + selectionStyle={selectedOperationDefinition.selectionStyle} + dateRange={dateRange} + {...services} + /> + ); + })} + + + ) : null} + {!selectedColumn || selectedOperationDefinition?.input === 'field' || (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field') ? ( @@ -325,7 +376,13 @@ export function DimensionEditor(props: DimensionEditorProps) { } incompleteOperation={incompleteOperation} onDeleteColumn={() => { - setStateWrapper(deleteColumn({ layer: state.layers[layerId], columnId })); + setStateWrapper( + deleteColumn({ + layer: state.layers[layerId], + columnId, + indexPattern: currentIndexPattern, + }) + ); }} onChoose={(choice) => { setStateWrapper( @@ -342,15 +399,6 @@ export function DimensionEditor(props: DimensionEditorProps) { ) : null} - {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( - - )} - {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ParamEditor && ( <> )} + + {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( + + )}
    @@ -432,11 +489,11 @@ export function DimensionEditor(props: DimensionEditorProps) { } function getErrorMessage( selectedColumn: IndexPatternColumn | undefined, - incompatibleSelectedOperationType: boolean, + incompleteOperation: boolean, input: 'none' | 'field' | 'fullReference' | undefined, fieldInvalid: boolean ) { - if (selectedColumn && incompatibleSelectedOperationType) { + if (selectedColumn && incompleteOperation) { if (input === 'field') { return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', { defaultMessage: 'To use this function, select a different field.', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 6bfeafd41c6b4..fc6c317365886 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -337,17 +337,124 @@ describe('IndexPatternDimensionEditorPanel', () => { const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; - expect(items.find(({ label }) => label === 'Minimum')!['data-test-subj']).not.toContain( + expect(items.find(({ id }) => id === 'min')!['data-test-subj']).not.toContain('incompatible'); + expect(items.find(({ id }) => id === 'date_histogram')!['data-test-subj']).toContain( + 'incompatible' + ); + // Incompatible because there is no date field + expect(items.find(({ id }) => id === 'cumulative_sum')!['data-test-subj']).toContain( 'incompatible' ); - expect(items.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain( + expect(items.find(({ id }) => id === 'filters')!['data-test-subj']).not.toContain( 'incompatible' ); + }); + + it('should indicate when a transition is invalid due to filterOperations', () => { + wrapper = mount( + meta.dataType === 'number' && !meta.isBucketed} + /> + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; - // Fieldless operation is compatible with field - expect(items.find(({ label }) => label === 'Filters')!['data-test-subj']).toContain( - 'compatible' + expect(items.find(({ id }) => id === 'min')!['data-test-subj']).toContain('incompatible'); + expect(items.find(({ id }) => id === 'cumulative_sum')!['data-test-subj']).toContain( + 'incompatible' + ); + }); + + it('should indicate that reference-based operations are not compatible when they are incomplete', () => { + wrapper = mount( + + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ id }) => id === 'derivative')!['data-test-subj']).toContain( + 'incompatible' + ); + expect(items.find(({ id }) => id === 'cumulative_sum')!['data-test-subj']).toContain( + 'incompatible' + ); + expect(items.find(({ id }) => id === 'moving_average')!['data-test-subj']).toContain( + 'incompatible' + ); + }); + + it('should indicate that reference-based operations are compatible sometimes', () => { + wrapper = mount( + + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ id }) => id === 'counter_rate')!['data-test-subj']).toContain( + 'incompatible' + ); + + expect(items.find(({ id }) => id === 'derivative')!['data-test-subj']).not.toContain( + 'incompatible' + ); + expect(items.find(({ id }) => id === 'moving_average')!['data-test-subj']).not.toContain( + 'incompatible' ); }); @@ -640,9 +747,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') .simulate('click'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-filters incompatible"]') - .simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-filters"]').simulate('click'); expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); @@ -854,6 +959,7 @@ describe('IndexPatternDimensionEditorPanel', () => { dataType: 'date', isBucketed: true, label: '', + customLabel: true, operationType: 'date_histogram', sourceField: 'ts', params: { @@ -872,6 +978,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columnId: 'col2', }; } + it('should not show custom options if time scaling is not available', () => { wrapper = mount( { layers: { first: { ...state.layers.first, + columnOrder: ['col1', 'col2'], columns: { ...state.layers.first.columns, col2: expect.objectContaining({ - sourceField: 'bytes', operationType: 'avg', - // Other parts of this don't matter for this test + sourceField: 'bytes', }), }, - columnOrder: ['col1', 'col2'], + incompleteColumns: {}, }, }, }, @@ -1237,7 +1344,9 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should indicate compatible fields when selecting the operation first', () => { wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + }); const options = wrapper .find(EuiComboBox) @@ -1317,12 +1426,18 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(items.map(({ label }: { label: React.ReactNode }) => label)).toEqual([ 'Average', 'Count', + 'Counter rate', + 'Cumulative sum', + 'Differences', 'Last value', 'Maximum', 'Median', 'Minimum', + 'Moving average', + 'Percentile', 'Sum', 'Unique count', + '\u00a0', ]); }); @@ -1536,4 +1651,109 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }); }); + + it('should hide the top level field selector when switching from non-reference to reference', () => { + wrapper = mount(); + + expect(wrapper.find('ReferenceEditor')).toHaveLength(0); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-derivative incompatible"]') + .simulate('click'); + + expect(wrapper.find('ReferenceEditor')).toHaveLength(1); + }); + + it('should hide the reference editors when switching from reference to non-reference', () => { + const stateWithReferences: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Differences of (incomplete)', + dataType: 'number', + isBucketed: false, + operationType: 'derivative', + references: ['col2'], + params: {}, + }, + }); + + wrapper = mount( + + ); + + expect(wrapper.find('ReferenceEditor')).toHaveLength(1); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-avg incompatible"]') + .simulate('click'); + + expect(wrapper.find('ReferenceEditor')).toHaveLength(0); + }); + + it('should show a warning when the current dimension is no longer configurable', () => { + const stateWithInvalidCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Invalid derivative', + dataType: 'number', + isBucketed: false, + operationType: 'derivative', + references: ['ref1'], + }, + }); + + wrapper = mount( + + ); + + expect( + wrapper + .find('[data-test-subj="lns-indexPatternDimension-derivative incompatible"]') + .find('EuiText[color="danger"]') + .first() + ).toBeTruthy(); + }); + + it('should remove options to select references when there are no time fields', () => { + const stateWithoutTime: IndexPatternPrivateState = { + ...getStateWithColumns({ + col1: { + label: 'Avg', + dataType: 'number', + isBucketed: false, + operationType: 'avg', + sourceField: 'bytes', + }, + }), + indexPatterns: { + 1: { + id: '1', + title: 'my-fake-index-pattern', + hasRestrictions: false, + fields: [ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + getFieldByName: getFieldByNameFactory([ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), + }, + }, + }; + + wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="lns-indexPatternDimension-derivative"]')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 406a32f62b2c7..fbdf90e6cc4c7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -41,6 +41,7 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { onDeleteColumn: () => void; existingFields: IndexPatternPrivateState['existingFields']; fieldIsInvalid: boolean; + markAllFieldsCompatible?: boolean; } export function FieldSelect({ @@ -53,6 +54,7 @@ export function FieldSelect({ onDeleteColumn, existingFields, fieldIsInvalid, + markAllFieldsCompatible, ...rest }: FieldSelectProps) { const { operationByField } = operationSupportMatrix; @@ -93,7 +95,7 @@ export function FieldSelect({ : operationByField[field]!.values().next().value, }, exists: containsData(field), - compatible: isCompatibleWithCurrentOperation(field), + compatible: markAllFieldsCompatible || isCompatibleWithCurrentOperation(field), }; }) .sort((a, b) => { @@ -163,6 +165,7 @@ export function FieldSelect({ currentIndexPattern, operationByField, existingFields, + markAllFieldsCompatible, ]); return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 817fdf637f001..9d55a9d5f7522 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -49,7 +49,7 @@ export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix supportedFieldsByOperation[operation.operationType] = new Set(); } supportedFieldsByOperation[operation.operationType]?.add(operation.field); - } else if (operation.type === 'none') { + } else { supportedOperationsWithoutField.add(operation.operationType); } }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx new file mode 100644 index 0000000000000..0891dd27fcf17 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -0,0 +1,436 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { ReactWrapper, ShallowWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiComboBox } from '@elastic/eui'; +import { mountWithIntl as mount } from '@kbn/test/jest'; +import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import { OperationMetadata } from '../../types'; +import { createMockedIndexPattern } from '../mocks'; +import { ReferenceEditor, ReferenceEditorProps } from './reference_editor'; +import { insertOrReplaceColumn } from '../operations'; +import { FieldSelect } from './field_select'; + +jest.mock('../operations'); + +describe('reference editor', () => { + let wrapper: ReactWrapper | ShallowWrapper; + let updateLayer: jest.Mock; + + function getDefaultArgs() { + return { + layer: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, + columnId: 'ref', + updateLayer, + selectionStyle: 'full' as const, + currentIndexPattern: createMockedIndexPattern(), + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + storage: {} as IStorageWrapper, + uiSettings: {} as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + http: {} as HttpSetup, + data: {} as DataPublicPluginStart, + }; + } + + beforeEach(() => { + updateLayer = jest.fn().mockImplementation((newLayer) => { + if (wrapper instanceof ReactWrapper) { + wrapper.setProps({ layer: newLayer }); + } + }); + + jest.clearAllMocks(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); + + it('should indicate that all functions and available fields are compatible in the empty state', () => { + wrapper = mount( + meta.dataType === 'number', + }} + /> + ); + + const functions = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-reference-function"]') + .prop('options'); + + expect(functions).not.toContainEqual( + expect.objectContaining({ 'data-test-subj': expect.stringContaining('Incompatible') }) + ); + + const fields = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(fields![0].options).not.toContainEqual( + expect.objectContaining({ 'data-test-subj': expect.stringContaining('Incompatible') }) + ); + expect(fields![1].options).not.toContainEqual( + expect.objectContaining({ 'data-test-subj': expect.stringContaining('Incompatible') }) + ); + }); + + it('should indicate functions and fields that are incompatible with the current', () => { + wrapper = mount( + meta.isBucketed, + }} + /> + ); + + const functions = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-reference-function"]') + .prop('options'); + expect(functions.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain( + 'incompatible' + ); + + const fields = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + expect( + fields![0].options!.find(({ label }) => label === 'timestampLabel')!['data-test-subj'] + ).toContain('Incompatible'); + }); + + it('should not update when selecting the same operation', () => { + wrapper = mount( + meta.dataType === 'number', + }} + /> + ); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-reference-function"]'); + const option = comboBox.prop('options')!.find(({ label }) => label === 'Average')!; + + act(() => { + comboBox.prop('onChange')!([option]); + }); + expect(insertOrReplaceColumn).not.toHaveBeenCalled(); + }); + + it('should keep the field when replacing an existing reference with a compatible function', () => { + wrapper = mount( + meta.dataType === 'number', + }} + /> + ); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-reference-function"]'); + const option = comboBox.prop('options')!.find(({ label }) => label === 'Maximum')!; + + act(() => { + comboBox.prop('onChange')!([option]); + }); + + expect(insertOrReplaceColumn).toHaveBeenCalledWith( + expect.objectContaining({ + op: 'max', + field: expect.objectContaining({ name: 'bytes' }), + }) + ); + }); + + it('should transition to another function with incompatible field', () => { + wrapper = mount( + true, + }} + /> + ); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-reference-function"]'); + const option = comboBox.prop('options')!.find(({ label }) => label === 'Date histogram')!; + + act(() => { + comboBox.prop('onChange')!([option]); + }); + + expect(insertOrReplaceColumn).toHaveBeenCalledWith( + expect.objectContaining({ + op: 'date_histogram', + field: undefined, + }) + ); + }); + + it('should hide the function selector when using a field-only selection style', () => { + wrapper = mount( + true, + }} + /> + ); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-reference-function"]'); + expect(comboBox).toHaveLength(0); + }); + + it('should pass the incomplete operation info to FieldSelect', () => { + wrapper = mount( + true, + }} + /> + ); + + const fieldSelect = wrapper.find(FieldSelect); + expect(fieldSelect.prop('fieldIsInvalid')).toEqual(true); + expect(fieldSelect.prop('selectedField')).toEqual('bytes'); + expect(fieldSelect.prop('selectedOperationType')).toEqual('avg'); + expect(fieldSelect.prop('incompleteOperation')).toEqual('max'); + expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(false); + }); + + it('should pass the incomplete field info to FieldSelect', () => { + wrapper = mount( + true, + }} + /> + ); + + const fieldSelect = wrapper.find(FieldSelect); + expect(fieldSelect.prop('fieldIsInvalid')).toEqual(false); + expect(fieldSelect.prop('selectedField')).toEqual('timestamp'); + expect(fieldSelect.prop('selectedOperationType')).toEqual('avg'); + expect(fieldSelect.prop('incompleteOperation')).toBeUndefined(); + }); + + it('should show the FieldSelect as invalid in the empty state for field-only forms', () => { + wrapper = mount( + true, + }} + /> + ); + + const fieldSelect = wrapper.find(FieldSelect); + expect(fieldSelect.prop('fieldIsInvalid')).toEqual(true); + expect(fieldSelect.prop('selectedField')).toBeUndefined(); + expect(fieldSelect.prop('selectedOperationType')).toBeUndefined(); + expect(fieldSelect.prop('incompleteOperation')).toBeUndefined(); + expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(true); + }); + + it('should show the ParamEditor for functions that offer one', () => { + wrapper = mount( + true, + }} + /> + ); + + expect(wrapper.find('[data-test-subj="lns-indexPattern-lastValue-sortField"]').exists()).toBe( + true + ); + }); + + it('should hide the ParamEditor for incomplete functions', () => { + wrapper = mount( + true, + }} + /> + ); + + expect(wrapper.find('[data-test-subj="lns-indexPattern-lastValue-sortField"]').exists()).toBe( + false + ); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx new file mode 100644 index 0000000000000..d73530ec8a920 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -0,0 +1,306 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './dimension_editor.scss'; +import _ from 'lodash'; +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { DateRange } from '../../../common'; +import type { OperationSupportMatrix } from './operation_support'; +import type { OperationType } from '../indexpattern'; +import { + operationDefinitionMap, + getOperationDisplay, + insertOrReplaceColumn, + deleteColumn, + isOperationAllowedAsReference, + FieldBasedIndexPatternColumn, + RequiredReference, +} from '../operations'; +import { FieldSelect } from './field_select'; +import { hasField } from '../utils'; +import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; +import { trackUiEvent } from '../../lens_ui_telemetry'; + +const operationPanels = getOperationDisplay(); + +export interface ReferenceEditorProps { + layer: IndexPatternLayer; + selectionStyle: 'full' | 'field'; + validation: RequiredReference; + columnId: string; + updateLayer: (newLayer: IndexPatternLayer) => void; + currentIndexPattern: IndexPattern; + existingFields: IndexPatternPrivateState['existingFields']; + dateRange: DateRange; + + // Services + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; + savedObjectsClient: SavedObjectsClientContract; + http: HttpSetup; + data: DataPublicPluginStart; +} + +export function ReferenceEditor(props: ReferenceEditorProps) { + const { + layer, + columnId, + updateLayer, + currentIndexPattern, + existingFields, + validation, + selectionStyle, + dateRange, + ...services + } = props; + + const column = layer.columns[columnId]; + const selectedOperationDefinition = column && operationDefinitionMap[column.operationType]; + + const ParamEditor = selectedOperationDefinition?.paramEditor; + + const incompleteInfo = layer.incompleteColumns ? layer.incompleteColumns[columnId] : undefined; + const incompleteOperation = incompleteInfo?.operationType; + const incompleteField = incompleteInfo?.sourceField ?? null; + + // Basically the operation support matrix, but different validation + const operationSupportMatrix: OperationSupportMatrix & { + operationTypes: Set; + } = useMemo(() => { + const operationTypes: Set = new Set(); + const operationWithoutField: Set = new Set(); + const operationByField: Partial>> = {}; + const fieldByOperation: Partial>> = {}; + Object.values(operationDefinitionMap) + .sort((op1, op2) => { + return op1.displayName.localeCompare(op2.displayName); + }) + .forEach((op) => { + if (op.input === 'field') { + const allFields = currentIndexPattern.fields.filter((field) => + isOperationAllowedAsReference({ + operationType: op.type, + validation, + field, + indexPattern: currentIndexPattern, + }) + ); + if (allFields.length) { + operationTypes.add(op.type); + fieldByOperation[op.type] = new Set(allFields.map(({ name }) => name)); + allFields.forEach((field) => { + if (!operationByField[field.name]) { + operationByField[field.name] = new Set(); + } + operationByField[field.name]?.add(op.type); + }); + } + } else if ( + isOperationAllowedAsReference({ + operationType: op.type, + validation, + indexPattern: currentIndexPattern, + }) + ) { + operationTypes.add(op.type); + operationWithoutField.add(op.type); + } + }); + return { + operationTypes, + operationWithoutField, + operationByField, + fieldByOperation, + }; + }, [currentIndexPattern, validation]); + + const functionOptions: Array> = Array.from( + operationSupportMatrix.operationTypes + ).map((operationType) => { + const def = operationDefinitionMap[operationType]; + const label = operationPanels[operationType].displayName; + const isCompatible = + !column || + (column && + hasField(column) && + def.input === 'field' && + operationSupportMatrix.fieldByOperation[operationType]?.has(column.sourceField)) || + (column && !hasField(column) && def.input !== 'field'); + + return { + label, + value: operationType, + className: 'lnsIndexPatternDimensionEditor__operation', + 'data-test-subj': `lns-indexPatternDimension-${operationType}${ + isCompatible ? '' : ' incompatible' + }`, + }; + }); + + function onChooseFunction(operationType: OperationType) { + if (column?.operationType === operationType) { + return; + } + const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType]; + if (column && 'sourceField' in column && possibleFieldNames?.has(column.sourceField)) { + // Reuse the current field if possible + updateLayer( + insertOrReplaceColumn({ + layer, + columnId, + op: operationType, + indexPattern: currentIndexPattern, + field: currentIndexPattern.getFieldByName(column.sourceField), + }) + ); + } else { + // If reusing the field is impossible, we generally can't choose for the user. + // The one exception is if the field is the only possible field, like Count of Records. + const possibleField = + possibleFieldNames?.size === 1 + ? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value) + : undefined; + + updateLayer( + insertOrReplaceColumn({ + layer, + columnId, + op: operationType, + indexPattern: currentIndexPattern, + field: possibleField, + }) + ); + } + trackUiEvent(`indexpattern_dimension_operation_${operationType}`); + return; + } + + const selectedOption = incompleteInfo?.operationType + ? [functionOptions.find(({ value }) => value === incompleteInfo.operationType)!] + : column + ? [functionOptions.find(({ value }) => value === column.operationType)!] + : []; + + // If the operationType is incomplete, the user needs to select a field- so + // the function is marked as valid. + const showOperationInvalid = !column && !Boolean(incompleteInfo?.operationType); + // The field is invalid if the operation has been updated without a field, + // or if we are in a field-only mode but empty state + const showFieldInvalid = + Boolean(incompleteInfo?.operationType) || (selectionStyle === 'field' && !column); + + return ( +
    +
    + {selectionStyle !== 'field' ? ( + <> + + { + if (choices.length === 0) { + updateLayer( + deleteColumn({ layer, columnId, indexPattern: currentIndexPattern }) + ); + return; + } + + trackUiEvent('indexpattern_dimension_field_changed'); + + onChooseFunction(choices[0].value!); + }} + /> + + + + ) : null} + + {!column || selectedOperationDefinition.input === 'field' ? ( + + { + updateLayer(deleteColumn({ layer, columnId, indexPattern: currentIndexPattern })); + }} + onChoose={(choice) => { + updateLayer( + insertOrReplaceColumn({ + layer, + columnId, + indexPattern: currentIndexPattern, + op: choice.operationType, + field: currentIndexPattern.getFieldByName(choice.field), + }) + ); + }} + /> + + ) : null} + + {column && !incompleteInfo && ParamEditor && ( + <> + + + )} +
    +
    + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 2e55abf4a429a..1f23fd3830477 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -474,6 +474,53 @@ describe('IndexPattern Data Source', () => { expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); + it('should add the suffix to the remap column id if provided by the operation', async () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['def', 'abc'], + columns: { + abc: { + label: '23rd percentile', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'percentile', + params: { + percentile: 23, + }, + }, + def: { + label: 'Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'source', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', + }, + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(Object.keys(JSON.parse(ast.chain[1].arguments.idMap[0] as string))).toEqual([ + 'col-0-def', + // col-1 is the auto naming of esasggs, abc is the specified column id, .23 is the generated suffix + 'col-1-abc.23', + ]); + }); + it('should add time_scale and format function if time scale is set and supported', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', @@ -858,165 +905,49 @@ describe('IndexPattern Data Source', () => { it('should return null for non-existant columns', () => { expect(publicAPI.getOperationForColumnId('col2')).toBe(null); }); - }); - }); - describe('#getErrorMessages', () => { - it('should detect a missing reference in a layer', () => { - const state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - dataType: 'number', - isBucketed: false, - label: 'Foo', - operationType: 'count', // <= invalid - sourceField: 'bytes', - }, - }, - }, - }, - currentIndexPatternId: '1', - }; - const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); - expect(messages).toHaveLength(1); - expect(messages![0]).toEqual({ - shortMessage: 'Invalid reference.', - longMessage: '"Foo" has an invalid reference.', - }); - }); - - it('should detect and batch missing references in a layer', () => { - const state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - dataType: 'number', - isBucketed: false, - label: 'Foo', - operationType: 'count', // <= invalid - sourceField: 'bytes', - }, - col2: { - dataType: 'number', - isBucketed: false, - label: 'Foo2', - operationType: 'count', // <= invalid - sourceField: 'memory', - }, - }, - }, - }, - currentIndexPatternId: '1', - }; - const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); - expect(messages).toHaveLength(1); - expect(messages![0]).toEqual({ - shortMessage: 'Invalid references.', - longMessage: '"Foo", "Foo2" have invalid reference.', - }); - }); + it('should return null for referenced columns', () => { + publicAPI = indexPatternDatasource.getPublicAPI({ + state: { + ...enrichBaseState(baseState), + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Sum', + dataType: 'number', + isBucketed: false, - it('should detect and batch missing references in multiple layers', () => { - const state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - dataType: 'number', - isBucketed: false, - label: 'Foo', - operationType: 'count', // <= invalid - sourceField: 'bytes', - }, - col2: { - dataType: 'number', - isBucketed: false, - label: 'Foo2', - operationType: 'count', // <= invalid - sourceField: 'memory', - }, - }, - }, - second: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - dataType: 'string', - isBucketed: false, - label: 'Foo', - operationType: 'count', // <= invalid - sourceField: 'source', - }, - }, - }, - }, - currentIndexPatternId: '1', - }; - const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); - expect(messages).toHaveLength(2); - expect(messages).toEqual([ - { - shortMessage: 'Invalid references on Layer 1.', - longMessage: 'Layer 1 has invalid references in "Foo", "Foo2".', - }, - { - shortMessage: 'Invalid reference on Layer 2.', - longMessage: 'Layer 2 has an invalid reference in "Foo".', - }, - ]); - }); + operationType: 'sum', + sourceField: 'test', + params: {}, + } as IndexPatternColumn, + col2: { + label: 'Cumulative sum', + dataType: 'number', + isBucketed: false, - it('should return no errors if all references are satified', () => { - const state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - dataType: 'number', - isBucketed: false, - label: 'Foo', - operationType: 'avg', - sourceField: 'bytes', + operationType: 'cumulative_sum', + references: ['col1'], + params: {}, + } as IndexPatternColumn, + }, }, }, }, - }, - currentIndexPatternId: '1', - }; - expect( - indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState) - ).toBeUndefined(); + layerId: 'first', + }); + expect(publicAPI.getOperationForColumnId('col1')).toEqual(null); + }); }); + }); - it('should return no errors with layers with no columns', () => { + describe('#getErrorMessages', () => { + it('should use the results of getErrorMessages directly when single layer', () => { + (getErrorMessages as jest.Mock).mockClear(); + (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']); const state: IndexPatternPrivateState = { indexPatternRefs: [], existingFields: {}, @@ -1031,10 +962,14 @@ describe('IndexPattern Data Source', () => { }, currentIndexPatternId: '1', }; - expect(indexPatternDatasource.getErrorMessages(state)).toBeUndefined(); + expect(indexPatternDatasource.getErrorMessages(state)).toEqual([ + { longMessage: 'error 1', shortMessage: '' }, + { longMessage: 'error 2', shortMessage: '' }, + ]); + expect(getErrorMessages).toHaveBeenCalledTimes(1); }); - it('should bubble up invalid configuration from operations', () => { + it('should prepend each error with its layer number on multi-layer chart', () => { (getErrorMessages as jest.Mock).mockClear(); (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']); const state: IndexPatternPrivateState = { @@ -1048,14 +983,19 @@ describe('IndexPattern Data Source', () => { columnOrder: [], columns: {}, }, + second: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, }, currentIndexPatternId: '1', }; expect(indexPatternDatasource.getErrorMessages(state)).toEqual([ - { shortMessage: 'error 1', longMessage: '' }, - { shortMessage: 'error 2', longMessage: '' }, + { longMessage: 'Layer 1 error: error 1', shortMessage: '' }, + { longMessage: 'Layer 1 error: error 2', shortMessage: '' }, ]); - expect(getErrorMessages).toHaveBeenCalledTimes(1); + expect(getErrorMessages).toHaveBeenCalledTimes(2); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2937b1cf05760..6c6bd2e1bb439 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -39,12 +39,7 @@ import { getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; -import { - getInvalidColumnsForLayer, - getInvalidLayers, - isDraggedField, - normalizeOperationDataType, -} from './utils'; +import { isDraggedField, normalizeOperationDataType } from './utils'; import { LayerPanel } from './layerpanel'; import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; @@ -55,7 +50,6 @@ import { mergeLayer } from './state_helpers'; import { Datasource, StateSetter } from '../index'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { deleteColumn, isReferenced } from './operations'; -import { FieldBasedIndexPatternColumn } from './operations/definitions/column_types'; import { Dragging } from '../drag_drop/providers'; export { OperationType, IndexPatternColumn, deleteColumn } from './operations'; @@ -162,10 +156,11 @@ export function getIndexPatternDatasource({ }, removeColumn({ prevState, layerId, columnId }) { + const indexPattern = prevState.indexPatterns[prevState.layers[layerId]?.indexPatternId]; return mergeLayer({ state: prevState, layerId, - newLayer: deleteColumn({ layer: prevState.layers[layerId], columnId }), + newLayer: deleteColumn({ layer: prevState.layers[layerId], columnId, indexPattern }), }); }, @@ -351,7 +346,9 @@ export function getIndexPatternDatasource({ const layer = state.layers[layerId]; if (layer && layer.columns[columnId]) { - return columnToOperation(layer.columns[columnId], columnLabelMap[columnId]); + if (!isReferenced(layer, columnId)) { + return columnToOperation(layer.columns[columnId], columnLabelMap[columnId]); + } } return null; }, @@ -369,91 +366,46 @@ export function getIndexPatternDatasource({ if (!state) { return; } - const invalidLayers = getInvalidLayers(state); - const layerErrors = Object.values(state.layers).flatMap((layer) => + const layerErrors = Object.values(state.layers).map((layer) => (getErrorMessages(layer) ?? []).map((message) => ({ - shortMessage: message, - longMessage: '', + shortMessage: '', // Not displayed currently + longMessage: message, })) ); - if (invalidLayers.length === 0) { - return layerErrors.length ? layerErrors : undefined; + // Single layer case, no need to explain more + if (layerErrors.length <= 1) { + return layerErrors[0]?.length ? layerErrors[0] : undefined; } - const realIndex = Object.values(state.layers) - .map((layer, i) => { - const filteredIndex = invalidLayers.indexOf(layer); - if (filteredIndex > -1) { - return [filteredIndex, i + 1]; - } - }) - .filter(Boolean) as Array<[number, number]>; - const invalidColumnsForLayer: string[][] = getInvalidColumnsForLayer( - invalidLayers, - state.indexPatterns - ); - const originalLayersList = Object.keys(state.layers); - - if (layerErrors.length || realIndex.length) { - return [ - ...layerErrors, - ...realIndex.map(([filteredIndex, layerIndex]) => { - const columnLabelsWithBrokenReferences: string[] = invalidColumnsForLayer[ - filteredIndex - ].map((columnId) => { - const column = invalidLayers[filteredIndex].columns[ - columnId - ] as FieldBasedIndexPatternColumn; - return column.label; - }); - - if (originalLayersList.length === 1) { - return { - shortMessage: i18n.translate( - 'xpack.lens.indexPattern.dataReferenceFailureShortSingleLayer', - { - defaultMessage: - 'Invalid {columns, plural, one {reference} other {references}}.', - values: { - columns: columnLabelsWithBrokenReferences.length, - }, - } - ), - longMessage: i18n.translate( - 'xpack.lens.indexPattern.dataReferenceFailureLongSingleLayer', - { - defaultMessage: `"{columns}" {columnsLength, plural, one {has an} other {have}} invalid reference.`, - values: { - columns: columnLabelsWithBrokenReferences.join('", "'), - columnsLength: columnLabelsWithBrokenReferences.length, - }, - } - ), - }; - } - return { - shortMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureShort', { - defaultMessage: - 'Invalid {columnsLength, plural, one {reference} other {references}} on Layer {layer}.', - values: { - layer: layerIndex, - columnsLength: columnLabelsWithBrokenReferences.length, - }, - }), - longMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureLong', { - defaultMessage: `Layer {layer} has {columnsLength, plural, one {an invalid} other {invalid}} {columnsLength, plural, one {reference} other {references}} in "{columns}".`, - values: { - layer: layerIndex, - columns: columnLabelsWithBrokenReferences.join('", "'), - columnsLength: columnLabelsWithBrokenReferences.length, - }, - }), - }; - }), - ]; - } + // For multiple layers we will prepend each error with the layer number + const messages = layerErrors.flatMap((errors, index) => { + return errors.map((error) => { + const { shortMessage, longMessage } = error; + return { + shortMessage: shortMessage + ? i18n.translate('xpack.lens.indexPattern.layerErrorWrapper', { + defaultMessage: 'Layer {position} error: {wrappedMessage}', + values: { + position: index + 1, + wrappedMessage: shortMessage, + }, + }) + : '', + longMessage: longMessage + ? i18n.translate('xpack.lens.indexPattern.layerErrorWrapper', { + defaultMessage: 'Layer {position} error: {wrappedMessage}', + values: { + position: index + 1, + wrappedMessage: longMessage, + }, + }) + : '', + }; + }); + }); + return messages.length ? messages : undefined; }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 9fbad553d441a..97a63de4f7ba2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -6,11 +6,12 @@ import { DatasourceSuggestion } from '../types'; import { generateId } from '../id_generator'; -import { IndexPatternPrivateState } from './types'; +import type { IndexPatternPrivateState } from './types'; import { getDatasourceSuggestionsForField, getDatasourceSuggestionsFromCurrentState, getDatasourceSuggestionsForVisualizeField, + IndexPatternSuggestion, } from './indexpattern_suggestions'; import { documentField } from './document_field'; import { getFieldByNameFactory } from './pure_helpers'; @@ -153,6 +154,7 @@ function testInitialState(): IndexPatternPrivateState { columns: { col1: { label: 'My Op', + customLabel: true, dataType: 'string', isBucketed: true, @@ -172,6 +174,19 @@ function testInitialState(): IndexPatternPrivateState { }; } +// Simplifies the debug output for failed test +function getSuggestionSubset( + suggestions: IndexPatternSuggestion[] +): Array> { + return suggestions.map((s) => { + const newSuggestion = { ...s } as Omit & { + state?: IndexPatternPrivateState; + }; + delete newSuggestion.state; + return newSuggestion; + }); +} + describe('IndexPattern Data Source suggestions', () => { beforeEach(async () => { let count = 0; @@ -698,6 +713,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, sourceField: 'source', label: 'values of source', + customLabel: true, operationType: 'terms', params: { orderBy: { type: 'column', columnId: 'colb' }, @@ -710,6 +726,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, sourceField: 'bytes', label: 'Avg of bytes', + customLabel: true, operationType: 'avg', }, }, @@ -733,7 +750,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, sourceField: 'timestamp', - label: 'date histogram of timestamp', + label: 'timestamp', operationType: 'date_histogram', params: { interval: 'w', @@ -744,6 +761,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, sourceField: 'bytes', label: 'Avg of bytes', + customLabel: true, operationType: 'avg', }, }, @@ -782,6 +800,7 @@ describe('IndexPattern Data Source suggestions', () => { }); it('puts a date histogram column after the last bucket column on date field', () => { + (generateId as jest.Mock).mockReturnValue('newid'); const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'timestamp', @@ -790,17 +809,16 @@ describe('IndexPattern Data Source suggestions', () => { aggregatable: true, searchable: true, }); - expect(suggestions).toContainEqual( expect.objectContaining({ state: expect.objectContaining({ layers: { previousLayer: initialState.layers.previousLayer, currentLayer: expect.objectContaining({ - columnOrder: ['cola', 'id1', 'colb'], + columnOrder: ['cola', 'newid', 'colb'], columns: { ...initialState.layers.currentLayer.columns, - id1: expect.objectContaining({ + newid: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), @@ -817,7 +835,7 @@ describe('IndexPattern Data Source suggestions', () => { columnId: 'cola', }), expect.objectContaining({ - columnId: 'id1', + columnId: 'newid', }), expect.objectContaining({ columnId: 'colb', @@ -845,6 +863,7 @@ describe('IndexPattern Data Source suggestions', () => { }); it('appends a terms column with default size on string field', () => { + (generateId as jest.Mock).mockReturnValue('newid'); const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'dest', @@ -853,17 +872,16 @@ describe('IndexPattern Data Source suggestions', () => { aggregatable: true, searchable: true, }); - expect(suggestions).toContainEqual( expect.objectContaining({ state: expect.objectContaining({ layers: { previousLayer: initialState.layers.previousLayer, currentLayer: expect.objectContaining({ - columnOrder: ['cola', 'id1', 'colb'], + columnOrder: ['cola', 'newid', 'colb'], columns: { ...initialState.layers.currentLayer.columns, - id1: expect.objectContaining({ + newid: expect.objectContaining({ operationType: 'terms', sourceField: 'dest', params: expect.objectContaining({ size: 3 }), @@ -877,6 +895,7 @@ describe('IndexPattern Data Source suggestions', () => { }); it('suggests both replacing and adding metric if only one other metric is set', () => { + (generateId as jest.Mock).mockReturnValue('newid'); const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'memory', @@ -885,7 +904,6 @@ describe('IndexPattern Data Source suggestions', () => { aggregatable: true, searchable: true, }); - expect(suggestions).toContainEqual( expect.objectContaining({ state: expect.objectContaining({ @@ -910,11 +928,11 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: expect.objectContaining({ currentLayer: expect.objectContaining({ - columnOrder: ['cola', 'colb', 'id1'], + columnOrder: ['cola', 'colb', 'newid'], columns: { cola: initialState.layers.currentLayer.columns.cola, colb: initialState.layers.currentLayer.columns.colb, - id1: expect.objectContaining({ + newid: expect.objectContaining({ operationType: 'avg', sourceField: 'memory', }), @@ -927,6 +945,7 @@ describe('IndexPattern Data Source suggestions', () => { }); it('adds a metric column on a number field if no other metrics set', () => { + (generateId as jest.Mock).mockReturnValue('newid'); const initialState = stateWithNonEmptyTables(); const modifiedState: IndexPatternPrivateState = { ...initialState, @@ -955,10 +974,10 @@ describe('IndexPattern Data Source suggestions', () => { layers: { previousLayer: modifiedState.layers.previousLayer, currentLayer: expect.objectContaining({ - columnOrder: ['cola', 'id1'], + columnOrder: ['cola', 'newid'], columns: { ...modifiedState.layers.currentLayer.columns, - id1: expect.objectContaining({ + newid: expect.objectContaining({ operationType: 'avg', sourceField: 'memory', }), @@ -1008,6 +1027,137 @@ describe('IndexPattern Data Source suggestions', () => { const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', documentField); expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' })); }); + + it('hides any referenced metrics when adding new metrics', () => { + (generateId as jest.Mock).mockReturnValue('newid'); + const initialState = stateWithNonEmptyTables(); + const modifiedState: IndexPatternPrivateState = { + ...initialState, + layers: { + currentLayer: { + indexPatternId: '1', + columnOrder: ['date', 'metric', 'ref'], + columns: { + date: { + label: '', + customLabel: true, + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { interval: 'auto' }, + }, + metric: { + label: '', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'avg', + sourceField: 'bytes', + }, + ref: { + label: '', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric'], + }, + }, + }, + }, + }; + const suggestions = getSuggestionSubset( + getDatasourceSuggestionsForField(modifiedState, '1', documentField) + ); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + isMultiRow: true, + changeType: 'extended', + label: undefined, + layerId: 'currentLayer', + columns: [ + { + columnId: 'date', + operation: expect.objectContaining({ dataType: 'date', isBucketed: true }), + }, + { + columnId: 'newid', + operation: expect.objectContaining({ dataType: 'number', isBucketed: false }), + }, + { + columnId: 'ref', + operation: expect.objectContaining({ dataType: 'number', isBucketed: false }), + }, + ], + }), + keptLayerIds: ['currentLayer'], + }) + ); + }); + + it('makes a suggestion to extending from an invalid state with a new metric', () => { + (generateId as jest.Mock).mockReturnValue('newid'); + const initialState = stateWithNonEmptyTables(); + const modifiedState: IndexPatternPrivateState = { + ...initialState, + layers: { + currentLayer: { + indexPatternId: '1', + columnOrder: ['metric', 'ref'], + columns: { + metric: { + label: '', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'avg', + sourceField: 'bytes', + }, + ref: { + label: '', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric'], + }, + }, + }, + }, + }; + const suggestions = getSuggestionSubset( + getDatasourceSuggestionsForField(modifiedState, '1', documentField) + ); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'extended', + columns: [ + { + columnId: 'newid', + operation: { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + scale: 'ratio', + }, + }, + { + columnId: 'ref', + operation: { + dataType: 'number', + isBucketed: false, + label: '', + scale: undefined, + }, + }, + ], + }), + }) + ); + }); }); describe('finding the layer that is using the current index pattern', () => { @@ -1121,6 +1271,7 @@ describe('IndexPattern Data Source suggestions', () => { }); }); }); + describe('#getDatasourceSuggestionsForVisualizeField', () => { describe('with no layer', () => { function stateWithoutLayer() { @@ -1218,6 +1369,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { cola: { label: 'My Op 2', + customLabel: true, dataType: 'string', isBucketed: true, @@ -1305,6 +1457,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { cola: { label: 'My Op', + customLabel: true, dataType: 'number', isBucketed: false, operationType: 'avg', @@ -1316,7 +1469,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - expect(getDatasourceSuggestionsFromCurrentState(state)).toContainEqual( + expect(getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state))).toContainEqual( expect.objectContaining({ table: { isMultiRow: true, @@ -1359,6 +1512,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { cola: { label: 'My Terms', + customLabel: true, dataType: 'string', isBucketed: true, operationType: 'terms', @@ -1372,6 +1526,7 @@ describe('IndexPattern Data Source suggestions', () => { }, colb: { label: 'My Op', + customLabel: true, dataType: 'number', isBucketed: false, operationType: 'avg', @@ -1383,7 +1538,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - expect(getDatasourceSuggestionsFromCurrentState(state)).toContainEqual( + expect(getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state))).toContainEqual( expect.objectContaining({ table: { isMultiRow: true, @@ -1442,6 +1597,7 @@ describe('IndexPattern Data Source suggestions', () => { }, colb: { label: 'My Op', + customLabel: true, dataType: 'number', isBucketed: true, operationType: 'range', @@ -1487,6 +1643,7 @@ describe('IndexPattern Data Source suggestions', () => { }, colb: { label: 'My Custom Range', + customLabel: true, dataType: 'string', isBucketed: true, operationType: 'range', @@ -1503,7 +1660,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - expect(getDatasourceSuggestionsFromCurrentState(state)).toContainEqual( + expect(getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state))).toContainEqual( expect.objectContaining({ table: { changeType: 'extended', @@ -1555,6 +1712,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { id1: { label: 'My Op', + customLabel: true, dataType: 'number', isBucketed: false, operationType: 'avg', @@ -1631,6 +1789,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { col1: { label: 'My Op', + customLabel: true, dataType: 'string', isBucketed: true, @@ -1644,6 +1803,7 @@ describe('IndexPattern Data Source suggestions', () => { }, col2: { label: 'My Op', + customLabel: true, dataType: 'string', isBucketed: true, @@ -1657,6 +1817,7 @@ describe('IndexPattern Data Source suggestions', () => { }, col3: { label: 'My Op', + customLabel: true, dataType: 'string', isBucketed: true, @@ -1670,6 +1831,7 @@ describe('IndexPattern Data Source suggestions', () => { }, col4: { label: 'My Op', + customLabel: true, dataType: 'number', isBucketed: false, @@ -1678,6 +1840,7 @@ describe('IndexPattern Data Source suggestions', () => { }, col5: { label: 'My Op', + customLabel: true, dataType: 'number', isBucketed: false, @@ -1691,34 +1854,29 @@ describe('IndexPattern Data Source suggestions', () => { }; const suggestions = getDatasourceSuggestionsFromCurrentState(state); - // 1 bucket col, 2 metric cols - isTableWithBucketColumns(suggestions[0], ['col1', 'col4', 'col5'], 1); + + // 3 bucket cols, 2 metric cols + isTableWithBucketColumns(suggestions[0], ['col1', 'col2', 'col3', 'col4', 'col5'], 3); // 1 bucket col, 1 metric col isTableWithBucketColumns(suggestions[1], ['col1', 'col4'], 1); // 2 bucket cols, 2 metric cols - isTableWithBucketColumns(suggestions[2], ['col1', 'col2', 'col4', 'col5'], 2); - - // 2 bucket cols, 1 metric col - isTableWithBucketColumns(suggestions[3], ['col1', 'col2', 'col4'], 2); - - // 3 bucket cols, 2 metric cols - isTableWithBucketColumns(suggestions[4], ['col1', 'col2', 'col3', 'col4', 'col5'], 3); + isTableWithBucketColumns(suggestions[2], ['col1', 'col2', 'col4'], 2); // 3 bucket cols, 1 metric col - isTableWithBucketColumns(suggestions[5], ['col1', 'col2', 'col3', 'col4'], 3); + isTableWithBucketColumns(suggestions[3], ['col1', 'col2', 'col3', 'col4'], 3); // first metric col - isTableWithMetricColumns(suggestions[6], ['col4']); + isTableWithMetricColumns(suggestions[4], ['col4']); // second metric col - isTableWithMetricColumns(suggestions[7], ['col5']); + isTableWithMetricColumns(suggestions[5], ['col5']); - expect(suggestions.length).toBe(8); + expect(suggestions.length).toBe(6); }); - it('returns an only metric version of a given table', () => { + it('returns an only metric version of a given table, but does not include current state as reduced', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { indexPatternRefs: [], @@ -1770,7 +1928,7 @@ describe('IndexPattern Data Source suggestions', () => { ...initialState.layers.first, columns: { id1: { - label: 'Date histogram', + label: 'field2', dataType: 'date', isBucketed: true, @@ -1794,8 +1952,34 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getDatasourceSuggestionsFromCurrentState(state); - expect(suggestions[1].table.columns[0].operation.label).toBe('Average of field1'); + const suggestions = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + expect(suggestions).not.toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'reduced', + columns: [ + expect.objectContaining({ + operation: expect.objectContaining({ label: 'field2' }), + }), + expect.objectContaining({ + operation: expect.objectContaining({ label: 'Average of field1' }), + }), + ], + }), + }) + ); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'reduced', + columns: [ + expect.objectContaining({ + operation: expect.objectContaining({ label: 'Average of field1' }), + }), + ], + }), + }) + ); }); it('returns an alternative metric for an only-metric table', () => { @@ -1848,9 +2032,18 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getDatasourceSuggestionsFromCurrentState(state); - expect(suggestions[0].table.columns.length).toBe(1); - expect(suggestions[0].table.columns[0].operation.label).toBe('Sum of field1'); + const suggestions = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + columns: [ + expect.objectContaining({ + operation: expect.objectContaining({ label: 'Sum of field1' }), + }), + ], + }), + }) + ); }); it('contains a reordering suggestion when there are exactly 2 buckets', () => { @@ -1909,7 +2102,7 @@ describe('IndexPattern Data Source suggestions', () => { ); }); - it('does not generate suggestions if invalid fields are referenced', () => { + it('will generate suggestions even if there are errors from missing fields', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { indexPatternRefs: [], @@ -1937,8 +2130,259 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getDatasourceSuggestionsFromCurrentState(state); - expect(suggestions).toEqual([]); + const suggestions = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: { + changeType: 'unchanged', + columns: [ + { + columnId: 'col1', + operation: { + dataType: 'string', + isBucketed: true, + label: 'My Op', + scale: undefined, + }, + }, + { + columnId: 'col2', + operation: { + dataType: 'string', + isBucketed: true, + label: 'Top 5', + scale: undefined, + }, + }, + ], + isMultiRow: true, + label: undefined, + layerId: 'first', + }, + }) + ); + }); + + describe('references', () => { + it('will extend the table with a date when starting in an invalid state', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + ...initialState.layers, + first: { + ...initialState.layers.first, + columnOrder: ['metric', 'ref', 'ref2'], + columns: { + metric: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'count', + sourceField: 'Records', + }, + ref: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric'], + }, + ref2: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric2'], + }, + }, + }, + }, + }; + + const result = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + + expect(result).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'extended', + layerId: 'first', + columns: [ + { + columnId: 'id1', + operation: { + dataType: 'date', + isBucketed: true, + label: 'timestampLabel', + scale: 'interval', + }, + }, + { + columnId: 'ref', + operation: { + dataType: 'number', + isBucketed: false, + label: 'Cumulative sum of Records', + scale: undefined, + }, + }, + { + columnId: 'ref2', + operation: { + dataType: 'number', + isBucketed: false, + label: 'Cumulative sum of (incomplete)', + scale: undefined, + }, + }, + ], + }), + keptLayerIds: ['first'], + }) + ); + }); + + it('will make an unchanged suggestion including incomplete references', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + ...initialState.layers, + first: { + ...initialState.layers.first, + columnOrder: ['date', 'ref', 'ref2'], + columns: { + date: { + label: '', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { interval: 'auto' }, + }, + ref: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric'], + }, + ref2: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric'], + }, + }, + }, + }, + }; + + const result = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + + expect(result).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'unchanged', + layerId: 'first', + columns: [ + { + columnId: 'date', + operation: { + dataType: 'date', + isBucketed: true, + label: '', + scale: undefined, + }, + }, + { + columnId: 'ref', + operation: { + dataType: 'number', + isBucketed: false, + label: '', + scale: undefined, + }, + }, + { + columnId: 'ref2', + operation: { + dataType: 'number', + isBucketed: false, + label: '', + scale: undefined, + }, + }, + ], + }), + keptLayerIds: ['first'], + }) + ); + }); + + it('will skip a reduced suggestion when handling multiple references', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + ...initialState.layers, + first: { + ...initialState.layers.first, + columnOrder: ['date', 'metric', 'metric2', 'ref', 'ref2'], + + columns: { + date: { + label: '', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { interval: 'auto' }, + }, + metric: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'count', + sourceField: 'Records', + }, + ref: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric'], + }, + metric2: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'count', + sourceField: 'Records', + }, + ref2: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric2'], + }, + }, + }, + }, + }; + + const result = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + + expect(result).not.toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'reduced', + }), + }) + ); + }); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index ebac396210a5c..9d7328b4dca37 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _, { partition } from 'lodash'; +import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { generateId } from '../id_generator'; import { DatasourceSuggestion, TableChangeType } from '../types'; @@ -17,8 +17,10 @@ import { operationDefinitionMap, IndexPatternColumn, OperationType, + getExistingColumnGroups, + isReferenced, } from './operations'; -import { hasField, hasInvalidColumns } from './utils'; +import { hasField } from './utils'; import { IndexPattern, IndexPatternPrivateState, @@ -27,7 +29,7 @@ import { } from './types'; import { documentField } from './document_field'; -type IndexPatternSugestion = DatasourceSuggestion; +export type IndexPatternSuggestion = DatasourceSuggestion; function buildSuggestion({ state, @@ -71,10 +73,13 @@ function buildSuggestion({ }, table: { - columns: columnOrder.map((columnId) => ({ - columnId, - operation: columnToOperation(columnMap[columnId]), - })), + columns: columnOrder + // Hide any referenced columns from what visualizations know about + .filter((columnId) => !isReferenced(layers[layerId]!, columnId)) + .map((columnId) => ({ + columnId, + operation: columnToOperation(columnMap[columnId]), + })), isMultiRow, layerId, changeType, @@ -89,8 +94,7 @@ export function getDatasourceSuggestionsForField( state: IndexPatternPrivateState, indexPatternId: string, field: IndexPatternField -): IndexPatternSugestion[] { - if (hasInvalidColumns(state)) return []; +): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); @@ -123,7 +127,7 @@ export function getDatasourceSuggestionsForVisualizeField( state: IndexPatternPrivateState, indexPatternId: string, fieldName: string -): IndexPatternSugestion[] { +): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); // Identify the field by the indexPatternId and the fieldName @@ -158,7 +162,7 @@ function getExistingLayerSuggestionsForField( const fieldInUse = Object.values(layer.columns).some( (column) => hasField(column) && column.sourceField === field.name ); - const suggestions: IndexPatternSugestion[] = []; + const suggestions: IndexPatternSuggestion[] = []; if (usableAsBucketOperation && !fieldInUse) { if ( @@ -221,8 +225,9 @@ function getExistingLayerSuggestionsForField( ); } - const [, metrics] = separateBucketColumns(layer); - if (metrics.length === 1) { + const [, metrics, references] = getExistingColumnGroups(layer); + // TODO: Write test for the case where we have exactly one metric and one reference. We shouldn't switch the inner metric. + if (metrics.length === 1 && references.length === 0) { const layerWithReplacedMetric = replaceColumn({ layer, indexPattern, @@ -257,7 +262,7 @@ function getEmptyLayerSuggestionsForField( layerId: string, indexPatternId: string, field: IndexPatternField -): IndexPatternSugestion[] { +): IndexPatternSuggestion[] { const indexPattern = state.indexPatterns[indexPatternId]; let newLayer: IndexPatternLayer | undefined; const bucketOperation = getBucketOperation(field); @@ -331,7 +336,6 @@ function createNewLayerWithMetricAggregation( export function getDatasourceSuggestionsFromCurrentState( state: IndexPatternPrivateState ): Array> { - if (hasInvalidColumns(state)) return []; const layers = Object.entries(state.layers || {}); if (layers.length > 1) { // Return suggestions that reduce the data to each layer individually @@ -372,12 +376,13 @@ export function getDatasourceSuggestionsFromCurrentState( }), ]); } + return _.flatten( Object.entries(state.layers || {}) .filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId) .map(([layerId, layer]) => { const indexPattern = state.indexPatterns[layer.indexPatternId]; - const [buckets, metrics] = separateBucketColumns(layer); + const [buckets, metrics, references] = getExistingColumnGroups(layer); const timeDimension = layer.columnOrder.find( (columnId) => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date' @@ -390,29 +395,22 @@ export function getDatasourceSuggestionsFromCurrentState( buckets.some((columnId) => layer.columns[columnId].dataType === 'number'); const suggestions: Array> = []; - if (metrics.length === 0) { - // intermediary chart without metric, don't try to suggest reduced versions - suggestions.push( - buildSuggestion({ - state, - layerId, - changeType: 'unchanged', - }) - ); - } else if (buckets.length === 0) { + + // Always suggest an unchanged table, including during invalid states + suggestions.push( + buildSuggestion({ + state, + layerId, + changeType: 'unchanged', + }) + ); + + if (!references.length && metrics.length && buckets.length === 0) { if (timeField) { // suggest current metric over time if there is a default time field suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField)); } suggestions.push(...createAlternativeMetricSuggestions(indexPattern, layerId, state)); - // also suggest simple current state - suggestions.push( - buildSuggestion({ - state, - layerId, - changeType: 'unchanged', - }) - ); } else { suggestions.push(...createSimplifiedTableSuggestions(state, layerId)); @@ -570,7 +568,11 @@ function createSuggestionWithDefaultDateHistogram( function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layerId: string) { const layer = state.layers[layerId]; - const [availableBucketedColumns, availableMetricColumns] = separateBucketColumns(layer); + const [ + availableBucketedColumns, + availableMetricColumns, + availableReferenceColumns, + ] = getExistingColumnGroups(layer); return _.flatten( availableBucketedColumns.map((_col, index) => { @@ -581,29 +583,30 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer columnOrder: [...bucketedColumns, ...availableMetricColumns], }; - if (availableMetricColumns.length > 1) { - return [ - allMetricsSuggestion, - { ...layer, columnOrder: [...bucketedColumns, availableMetricColumns[0]] }, - ]; + if (availableBucketedColumns.length <= 1 || availableReferenceColumns.length) { + // Don't simplify when dealing with single-bucket table. Also don't break + // reference-based columns by removing buckets. + return []; + } else if (availableMetricColumns.length > 1) { + return [{ ...layer, columnOrder: [...bucketedColumns, availableMetricColumns[0]] }]; } else { return allMetricsSuggestion; } }) ) .concat( - availableMetricColumns.map((columnId) => { - // build suggestions with only metrics - return { ...layer, columnOrder: [columnId] }; - }) + availableReferenceColumns.length + ? [] + : availableMetricColumns.map((columnId) => { + return { ...layer, columnOrder: [columnId] }; + }) ) .map((updatedLayer) => { return buildSuggestion({ state, layerId, updatedLayer, - changeType: - layer.columnOrder.length === updatedLayer.columnOrder.length ? 'unchanged' : 'reduced', + changeType: 'reduced', label: updatedLayer.columnOrder.length === 1 ? getMetricSuggestionTitle(updatedLayer, availableMetricColumns.length === 1) @@ -623,7 +626,3 @@ function getMetricSuggestionTitle(layer: IndexPatternLayer, onlyMetric: boolean) 'Title of a suggested chart containing only a single numerical metric calculated over all available data', }); } - -function separateBucketColumns(layer: IndexPatternLayer) { - return partition(layer.columnOrder, (columnId) => layer.columns[columnId].isBucketed); -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index ff900134df9a1..3d10080aea0c6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -42,6 +42,8 @@ export const { getErrorMessages, isReferenced, resetIncomplete, + isOperationAllowedAsReference, + canTransition, } = actualHelpers; export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index 0cfba4cfc739f..4fd045c17740d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -9,6 +9,7 @@ import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '. import { IndexPatternLayer } from '../../../types'; import { buildLabelFunction, + getErrorsForDateReference, checkForDateHistogram, dateBasedOperationToExpression, hasDateField, @@ -52,15 +53,18 @@ export const counterRateOperation: OperationDefinition< validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], - getPossibleOperation: () => { - return { - dataType: 'number', - isBucketed: false, - scale: 'ratio', - }; + getPossibleOperation: (indexPattern) => { + if (hasDateField(indexPattern)) { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + } }, getDefaultLabel: (column, indexPattern, columns) => { - return ofName(columns[column.references[0]]?.label, column.timeScale); + const ref = columns[column.references[0]]; + return ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined, column.timeScale); }, toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'lens_counter_rate'); @@ -69,7 +73,7 @@ export const counterRateOperation: OperationDefinition< const metric = layer.columns[referenceIds[0]]; const timeScale = previousColumn?.timeScale || DEFAULT_TIME_SCALE; return { - label: ofName(metric?.label, timeScale), + label: ofName(metric && 'sourceField' in metric ? metric.sourceField : undefined, timeScale), dataType: 'number', operationType: 'counter_rate', isBucketed: false, @@ -88,13 +92,22 @@ export const counterRateOperation: OperationDefinition< isTransferable: (column, newIndexPattern) => { return hasDateField(newIndexPattern); }, - getErrorMessage: (layer: IndexPatternLayer) => { - return checkForDateHistogram( + getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { + return getErrorsForDateReference( layer, + columnId, i18n.translate('xpack.lens.indexPattern.counterRate', { defaultMessage: 'Counter rate', }) ); }, + getDisabledStatus(indexPattern, layer) { + return checkForDateHistogram( + layer, + i18n.translate('xpack.lens.indexPattern.counterRate', { + defaultMessage: 'Counter rate', + }) + )?.join(', '); + }, timeScalingMode: 'mandatory', }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index 9244aaaf90ab7..7067b6470bec7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -7,12 +7,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; -import { checkForDateHistogram, dateBasedOperationToExpression } from './utils'; +import { + checkForDateHistogram, + getErrorsForDateReference, + dateBasedOperationToExpression, + hasDateField, +} from './utils'; import { OperationDefinition } from '..'; const ofName = (name?: string) => { return i18n.translate('xpack.lens.indexPattern.cumulativeSumOf', { - defaultMessage: 'Cumulative sum rate of {name}', + defaultMessage: 'Cumulative sum of {name}', values: { name: name ?? @@ -46,23 +51,26 @@ export const cumulativeSumOperation: OperationDefinition< validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], - getPossibleOperation: () => { - return { - dataType: 'number', - isBucketed: false, - scale: 'ratio', - }; + getPossibleOperation: (indexPattern) => { + if (hasDateField(indexPattern)) { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + } }, getDefaultLabel: (column, indexPattern, columns) => { - return ofName(columns[column.references[0]]?.label); + const ref = columns[column.references[0]]; + return ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined); }, toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'cumulative_sum'); }, buildColumn: ({ referenceIds, previousColumn, layer }) => { - const metric = layer.columns[referenceIds[0]]; + const ref = layer.columns[referenceIds[0]]; return { - label: ofName(metric?.label), + label: ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined), dataType: 'number', operationType: 'cumulative_sum', isBucketed: false, @@ -80,12 +88,21 @@ export const cumulativeSumOperation: OperationDefinition< isTransferable: () => { return true; }, - getErrorMessage: (layer: IndexPatternLayer) => { - return checkForDateHistogram( + getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { + return getErrorsForDateReference( layer, + columnId, i18n.translate('xpack.lens.indexPattern.cumulativeSum', { defaultMessage: 'Cumulative sum', }) ); }, + getDisabledStatus(indexPattern, layer) { + return checkForDateHistogram( + layer, + i18n.translate('xpack.lens.indexPattern.cumulativeSum', { + defaultMessage: 'Cumulative sum', + }) + )?.join(', '); + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx index 41fe361c7ba9c..358046ad5bfb9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx @@ -10,6 +10,7 @@ import { IndexPatternLayer } from '../../../types'; import { buildLabelFunction, checkForDateHistogram, + getErrorsForDateReference, dateBasedOperationToExpression, hasDateField, } from './utils'; @@ -51,23 +52,29 @@ export const derivativeOperation: OperationDefinition< validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], - getPossibleOperation: () => { - return { - dataType: 'number', - isBucketed: false, - scale: 'ratio', - }; + getPossibleOperation: (indexPattern) => { + if (hasDateField(indexPattern)) { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + } }, getDefaultLabel: (column, indexPattern, columns) => { - return ofName(columns[column.references[0]]?.label, column.timeScale); + const ref = columns[column.references[0]]; + return ofName(ref && 'sourceField' in ref ? ref.sourceField : undefined, column.timeScale); }, toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'derivative'); }, buildColumn: ({ referenceIds, previousColumn, layer }) => { - const metric = layer.columns[referenceIds[0]]; + const ref = layer.columns[referenceIds[0]]; return { - label: ofName(metric?.label, previousColumn?.timeScale), + label: ofName( + ref && 'sourceField' in ref ? ref.sourceField : undefined, + previousColumn?.timeScale + ), dataType: 'number', operationType: 'derivative', isBucketed: false, @@ -87,13 +94,22 @@ export const derivativeOperation: OperationDefinition< return hasDateField(newIndexPattern); }, onOtherColumnChanged: adjustTimeScaleOnOtherColumnChange, - getErrorMessage: (layer: IndexPatternLayer) => { - return checkForDateHistogram( + getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { + return getErrorsForDateReference( layer, + columnId, i18n.translate('xpack.lens.indexPattern.derivative', { defaultMessage: 'Differences', }) ); }, + getDisabledStatus(indexPattern, layer) { + return checkForDateHistogram( + layer, + i18n.translate('xpack.lens.indexPattern.derivative', { + defaultMessage: 'Differences', + }) + )?.join(', '); + }, timeScalingMode: 'optional', }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index 59d5924b9a370..d9805b337c000 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -14,11 +14,12 @@ import { IndexPatternLayer } from '../../../types'; import { buildLabelFunction, checkForDateHistogram, + getErrorsForDateReference, dateBasedOperationToExpression, hasDateField, } from './utils'; import { updateColumnParam } from '../../layer_helpers'; -import { useDebounceWithOptions } from '../helpers'; +import { isValidNumber, useDebounceWithOptions } from '../helpers'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import type { OperationDefinition, ParamEditorProps } from '..'; @@ -50,7 +51,7 @@ export const movingAverageOperation: OperationDefinition< type: 'moving_average', priority: 1, displayName: i18n.translate('xpack.lens.indexPattern.movingAverage', { - defaultMessage: 'Moving Average', + defaultMessage: 'Moving average', }), input: 'fullReference', selectionStyle: 'full', @@ -60,12 +61,14 @@ export const movingAverageOperation: OperationDefinition< validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], - getPossibleOperation: () => { - return { - dataType: 'number', - isBucketed: false, - scale: 'ratio', - }; + getPossibleOperation: (indexPattern) => { + if (hasDateField(indexPattern)) { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + } }, getDefaultLabel: (column, indexPattern, columns) => { return ofName(columns[column.references[0]]?.label, column.timeScale); @@ -99,14 +102,23 @@ export const movingAverageOperation: OperationDefinition< return hasDateField(newIndexPattern); }, onOtherColumnChanged: adjustTimeScaleOnOtherColumnChange, - getErrorMessage: (layer: IndexPatternLayer) => { - return checkForDateHistogram( + getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { + return getErrorsForDateReference( layer, + columnId, i18n.translate('xpack.lens.indexPattern.movingAverage', { - defaultMessage: 'Moving Average', + defaultMessage: 'Moving average', }) ); }, + getDisabledStatus(indexPattern, layer) { + return checkForDateHistogram( + layer, + i18n.translate('xpack.lens.indexPattern.movingAverage', { + defaultMessage: 'Moving average', + }) + )?.join(', '); + }, timeScalingMode: 'optional', }; @@ -120,10 +132,8 @@ function MovingAverageParamEditor({ useDebounceWithOptions( () => { - if (inputValue === '') { - return; - } - const inputNumber = Number(inputValue); + if (!isValidNumber(inputValue, true, undefined, 1)) return; + const inputNumber = parseInt(inputValue, 10); updateLayer( updateColumnParam({ layer, @@ -137,6 +147,7 @@ function MovingAverageParamEditor({ 256, [inputValue] ); + return ( ) => setInputValue(e.target.value)} + min={1} + step={1} + isInvalid={!isValidNumber(inputValue)} /> ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts new file mode 100644 index 0000000000000..403f2b87ac86e --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.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 { checkReferences } from './utils'; +import { operationDefinitionMap } from '..'; +import { createMockedReferenceOperation } from '../../mocks'; + +// Mock prevents issue with circular loading +jest.mock('..'); + +describe('utils', () => { + beforeEach(() => { + // @ts-expect-error test-only operation type + operationDefinitionMap.testReference = createMockedReferenceOperation(); + }); + + describe('checkReferences', () => { + it('should show an error if the reference is missing', () => { + expect( + checkReferences( + { + columns: { + ref: { + label: 'Label', + // @ts-expect-error test-only operation type + operationType: 'testReference', + isBucketed: false, + dataType: 'number', + references: ['missing'], + }, + }, + columnOrder: ['ref'], + indexPatternId: '', + }, + 'ref' + ) + ).toEqual(['"Label" is not fully configured']); + }); + + it('should show an error if the reference is not allowed per the requirements', () => { + expect( + checkReferences( + { + columns: { + ref: { + label: 'Label', + // @ts-expect-error test-only operation type + operationType: 'testReference', + isBucketed: false, + dataType: 'number', + references: ['invalid'], + }, + invalid: { + label: 'Date', + operationType: 'date_histogram', + isBucketed: true, + dataType: 'date', + sourceField: 'timestamp', + params: { interval: 'auto' }, + }, + }, + columnOrder: ['invalid', 'ref'], + indexPatternId: '', + }, + 'ref' + ) + ).toEqual(['Dimension "Label" is configured incorrectly']); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index bac45f683e444..8058f0a264229 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -5,11 +5,13 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { TimeScaleUnit } from '../../../time_scale'; -import { IndexPattern, IndexPatternLayer } from '../../../types'; +import type { ExpressionFunctionAST } from '@kbn/interpreter/common'; +import type { TimeScaleUnit } from '../../../time_scale'; +import type { IndexPattern, IndexPatternLayer } from '../../../types'; import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils'; -import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import type { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { isColumnValidAsReference } from '../../layer_helpers'; +import { operationDefinitionMap } from '..'; export const buildLabelFunction = (ofName: (name?: string) => string) => ( name?: string, @@ -41,6 +43,61 @@ export function checkForDateHistogram(layer: IndexPatternLayer, name: string) { ]; } +export function checkReferences(layer: IndexPatternLayer, columnId: string) { + const column = layer.columns[columnId] as ReferenceBasedIndexPatternColumn; + + const errors: string[] = []; + + column.references.forEach((referenceId, index) => { + if (!layer.columns[referenceId]) { + errors.push( + i18n.translate('xpack.lens.indexPattern.missingReferenceError', { + defaultMessage: '"{dimensionLabel}" is not fully configured', + values: { + dimensionLabel: column.label, + }, + }) + ); + } else { + const referenceColumn = layer.columns[referenceId]!; + const definition = operationDefinitionMap[column.operationType]; + if (definition.input !== 'fullReference') { + throw new Error('inconsistent state - column is not a reference operation'); + } + const requirements = definition.requiredReferences[index]; + const isValid = isColumnValidAsReference({ + validation: requirements, + column: referenceColumn, + }); + + if (!isValid) { + errors.push( + i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', { + defaultMessage: 'Dimension "{dimensionLabel}" is configured incorrectly', + values: { + dimensionLabel: column.label, + }, + }) + ); + } + } + }); + return errors.length ? errors : undefined; +} + +export function getErrorsForDateReference( + layer: IndexPatternLayer, + columnId: string, + name: string +) { + const dateErrors = checkForDateHistogram(layer, name) ?? []; + const referenceErrors = checkReferences(layer, columnId) ?? []; + if (dateErrors.length || referenceErrors.length) { + return [...dateErrors, ...referenceErrors]; + } + return; +} + export function hasDateField(indexPattern: IndexPattern) { return indexPattern.fields.some((field) => field.type === 'date'); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 95e905f6021be..970f56020c7cd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -10,7 +10,7 @@ import { buildExpressionFunction } from '../../../../../../../src/plugins/expres import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; -import { getInvalidFieldMessage } from './helpers'; +import { getInvalidFieldMessage, getSafeName } from './helpers'; const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); @@ -21,7 +21,9 @@ const IS_BUCKETED = false; function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.cardinalityOf', { defaultMessage: 'Unique count of {name}', - values: { name }, + values: { + name, + }, }); } @@ -58,8 +60,7 @@ export const cardinalityOperation: OperationDefinition - ofName(indexPattern.getFieldByName(column.sourceField)!.displayName), + getDefaultLabel: (column, indexPattern) => ofName(getSafeName(column.sourceField, indexPattern)), buildColumn({ field, previousColumn }) { return { label: ofName(field.displayName), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 0d8ed44f528a8..06d330a4a7eb2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -69,7 +69,12 @@ export const countOperation: OperationDefinition + adjustTimeScaleOnOtherColumnChange( + layer, + thisColumnId, + changedColumnId + ), toEsAggsFn: (column, columnId) => { return buildExpressionFunction('aggCount', { id: columnId, diff --git a/x-pack/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 index eadcf8384b1dd..abd033c0db4cf 100644 --- a/x-pack/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 @@ -199,7 +199,8 @@ describe('date_histogram', () => { const esAggsFn = dateHistogramOperation.toEsAggsFn( layer.columns.col1 as DateHistogramIndexPatternColumn, 'col1', - indexPattern1 + indexPattern1, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -250,7 +251,8 @@ describe('date_histogram', () => { }, }, ]), - } + }, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -689,4 +691,32 @@ describe('date_histogram', () => { expect(instance.find('[data-test-subj="lensDateHistogramValue"]').exists()).toBeFalsy(); }); }); + + describe('getDefaultLabel', () => { + it('should not throw when the source field is not located', () => { + expect( + dateHistogramOperation.getDefaultLabel( + { + label: '', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'missing', + params: { interval: 'auto' }, + }, + indexPattern1, + { + col1: { + label: '', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'missing', + params: { interval: 'auto' }, + }, + } + ) + ).toEqual('Missing field'); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index cdd1ccad96a99..a41cc88c4f292 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -28,7 +28,7 @@ import { search, } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; -import { getInvalidFieldMessage } from './helpers'; +import { getInvalidFieldMessage, getSafeName } from './helpers'; const { isValidInterval } = search.aggs; const autoInterval = 'auto'; @@ -67,8 +67,7 @@ export const dateHistogramOperation: OperationDefinition< }; } }, - getDefaultLabel: (column, indexPattern) => - indexPattern.getFieldByName(column.sourceField)!.displayName, + getDefaultLabel: (column, indexPattern) => getSafeName(column.sourceField, indexPattern), buildColumn({ field }) { let interval = autoInterval; let timeZone: string | undefined; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index cf57c35f6f68b..86767fbc8b469 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -83,7 +83,8 @@ describe('filters', () => { const esAggsFn = filtersOperation.toEsAggsFn( layer.columns.col1 as FiltersIndexPatternColumn, 'col1', - createMockedIndexPattern() + createMockedIndexPattern(), + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts new file mode 100644 index 0000000000000..04e04816d98ef --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockedIndexPattern } from '../../mocks'; +import { getInvalidFieldMessage } from './helpers'; + +describe('helpers', () => { + describe('getInvalidFieldMessage', () => { + it('return an error if a field was removed', () => { + const messages = getInvalidFieldMessage( + { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'bytes', + }, + createMockedIndexPattern() + ); + expect(messages).toHaveLength(1); + expect(messages![0]).toEqual('Field bytes was not found'); + }); + + it('returns an error if a field is the wrong type', () => { + const messages = getInvalidFieldMessage( + { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'avg', // <= invalid + sourceField: 'timestamp', + }, + createMockedIndexPattern() + ); + expect(messages).toHaveLength(1); + expect(messages![0]).toEqual('Field timestamp was not found'); + }); + + it('returns no message if all fields are matching', () => { + const messages = getInvalidFieldMessage( + { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'avg', + sourceField: 'bytes', + }, + createMockedIndexPattern() + ); + expect(messages).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index 640a357d9a7a4..29148052cee8e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -7,7 +7,7 @@ import { useRef } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; -import { operationDefinitionMap } from '.'; +import { IndexPatternColumn, operationDefinitionMap } from '.'; import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPattern } from '../../types'; @@ -62,3 +62,38 @@ export function getInvalidFieldMessage( ] : undefined; } + +export function getEsAggsSuffix(column: IndexPatternColumn) { + const operationDefinition = operationDefinitionMap[column.operationType]; + return operationDefinition.input === 'field' && operationDefinition.getEsAggsSuffix + ? operationDefinition.getEsAggsSuffix(column) + : ''; +} + +export function getSafeName(name: string, indexPattern: IndexPattern): string { + const field = indexPattern.getFieldByName(name); + return field + ? field.displayName + : i18n.translate('xpack.lens.indexPattern.missingFieldLabel', { + defaultMessage: 'Missing field', + }); +} + +export function isValidNumber( + inputValue: string | number | null | undefined, + integer?: boolean, + upperBound?: number, + lowerBound?: number +) { + const inputValueAsNumber = Number(inputValue); + return ( + inputValue !== '' && + inputValue !== null && + inputValue !== undefined && + !Number.isNaN(inputValueAsNumber) && + Number.isFinite(inputValueAsNumber) && + (!integer || Number.isInteger(inputValueAsNumber)) && + (upperBound === undefined || inputValueAsNumber <= upperBound) && + (lowerBound === undefined || inputValueAsNumber >= lowerBound) + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 6431dac7b381d..36c9cf75d2b6c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -9,6 +9,7 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { termsOperation, TermsIndexPatternColumn } from './terms'; import { filtersOperation, FiltersIndexPatternColumn } from './filters'; import { cardinalityOperation, CardinalityIndexPatternColumn } from './cardinality'; +import { percentileOperation, PercentileIndexPatternColumn } from './percentile'; import { minOperation, MinIndexPatternColumn, @@ -58,6 +59,7 @@ export type IndexPatternColumn = | CardinalityIndexPatternColumn | SumIndexPatternColumn | MedianIndexPatternColumn + | PercentileIndexPatternColumn | CountIndexPatternColumn | LastValueIndexPatternColumn | CumulativeSumIndexPatternColumn @@ -82,6 +84,7 @@ const internalOperationDefinitions = [ cardinalityOperation, sumOperation, medianOperation, + percentileOperation, lastValueOperation, countOperation, rangeOperation, @@ -96,6 +99,7 @@ export { rangeOperation } from './ranges'; export { filtersOperation } from './filters'; export { dateHistogramOperation } from './date_histogram'; export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; +export { percentileOperation } from './percentile'; export { countOperation } from './count'; export { lastValueOperation } from './last_value'; export { @@ -152,8 +156,9 @@ interface BaseOperationDefinitionProps { * return an updated column. If not implemented, the `id` function is used instead. */ onOtherColumnChanged?: ( - currentColumn: C, - columns: Partial> + layer: IndexPatternLayer, + thisColumnId: string, + changedColumnId: string ) => C; /** * React component for operation specific settings shown in the popover editor @@ -176,7 +181,7 @@ interface BaseOperationDefinitionProps { * but disable it from usage, this function returns the string describing * the status. Otherwise it returns undefined */ - getDisabledStatus?: (indexPattern: IndexPattern) => string | undefined; + getDisabledStatus?: (indexPattern: IndexPattern, layer: IndexPatternLayer) => string | undefined; /** * Validate that the operation has the right preconditions in the state. For example: * @@ -222,7 +227,12 @@ interface FieldlessOperationDefinition { * Function turning a column into an agg config passed to the `esaggs` function * together with the agg configs returned from other columns. */ - toEsAggsFn: (column: C, columnId: string, indexPattern: IndexPattern) => ExpressionAstFunction; + toEsAggsFn: ( + column: C, + columnId: string, + indexPattern: IndexPattern, + layer: IndexPatternLayer + ) => ExpressionAstFunction; } interface FieldBasedOperationDefinition { @@ -261,7 +271,19 @@ interface FieldBasedOperationDefinition { * Function turning a column into an agg config passed to the `esaggs` function * together with the agg configs returned from other columns. */ - toEsAggsFn: (column: C, columnId: string, indexPattern: IndexPattern) => ExpressionAstFunction; + toEsAggsFn: ( + column: C, + columnId: string, + indexPattern: IndexPattern, + layer: IndexPatternLayer + ) => ExpressionAstFunction; + /** + * Optional function to return the suffix used for ES bucket paths and esaggs column id. + * This is relevant for multi metrics to pick the right value. + * + * @param column The current column + */ + getEsAggsSuffix?: (column: C) => string; /** * Validate that the operation has the right preconditions in the state. For example: * @@ -314,9 +336,9 @@ interface FullReferenceOperationDefinition { ) => ReferenceBasedIndexPatternColumn & C; /** * Returns the meta data of the operation if applied. Undefined - * if the field is not applicable. + * if the operation can't be added with these fields. */ - getPossibleOperation: () => OperationMetadata; + getPossibleOperation: (indexPattern: IndexPattern) => OperationMetadata | undefined; /** * A chain of expression functions which will transform the table */ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index 817958aee5490..96b12a714e613 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -69,7 +69,8 @@ describe('last_value', () => { const esAggsFn = lastValueOperation.toEsAggsFn( { ...lastValueColumn, params: { ...lastValueColumn.params } }, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -311,13 +312,13 @@ describe('last_value', () => { it('should return disabledStatus if indexPattern does contain date field', () => { const indexPattern = createMockedIndexPattern(); - expect(lastValueOperation.getDisabledStatus!(indexPattern)).toEqual(undefined); + expect(lastValueOperation.getDisabledStatus!(indexPattern, layer)).toEqual(undefined); const indexPatternWithoutTimeFieldName = { ...indexPattern, timeFieldName: undefined, }; - expect(lastValueOperation.getDisabledStatus!(indexPatternWithoutTimeFieldName)).toEqual( + expect(lastValueOperation.getDisabledStatus!(indexPatternWithoutTimeFieldName, layer)).toEqual( undefined ); @@ -326,7 +327,10 @@ describe('last_value', () => { fields: indexPattern.fields.filter((f) => f.type !== 'date'), }; - const disabledStatus = lastValueOperation.getDisabledStatus!(indexPatternWithoutTimefields); + const disabledStatus = lastValueOperation.getDisabledStatus!( + indexPatternWithoutTimefields, + layer + ); expect(disabledStatus).toEqual( 'This function requires the presence of a date field in your index' ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 7b5aee860654a..256ef7f75676d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -13,12 +13,14 @@ import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField, IndexPattern } from '../../types'; import { updateColumnParam } from '../layer_helpers'; import { DataType } from '../../../types'; -import { getInvalidFieldMessage } from './helpers'; +import { getInvalidFieldMessage, getSafeName } from './helpers'; function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.lastValueOf', { defaultMessage: 'Last value of {name}', - values: { name }, + values: { + name, + }, }); } @@ -87,8 +89,7 @@ export const lastValueOperation: OperationDefinition - indexPattern.getFieldByName(column.sourceField)!.displayName, + getDefaultLabel: (column, indexPattern) => ofName(getSafeName(column.sourceField, indexPattern)), input: 'field', onFieldChange: (oldColumn, field) => { const newParams = { ...oldColumn.params }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index a886bfdaad325..470a5407b2589 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { OperationDefinition } from './index'; -import { getInvalidFieldMessage } from './helpers'; +import { getInvalidFieldMessage, getSafeName } from './helpers'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn, @@ -45,11 +45,11 @@ function buildMetricOperation>({ optionalTimeScaling?: boolean; }) { const labelLookup = (name: string, column?: BaseIndexPatternColumn) => { - const rawLabel = ofName(name); + const label = ofName(name); if (!optionalTimeScaling) { - return rawLabel; + return label; } - return adjustTimeScaleLabelSuffix(rawLabel, undefined, column?.timeScale); + return adjustTimeScaleLabelSuffix(label, undefined, column?.timeScale); }; return { @@ -81,21 +81,26 @@ function buildMetricOperation>({ (!newField.aggregationRestrictions || newField.aggregationRestrictions![type]) ); }, - onOtherColumnChanged: (column, otherColumns) => - optionalTimeScaling ? adjustTimeScaleOnOtherColumnChange(column, otherColumns) : column, + onOtherColumnChanged: (layer, thisColumnId, changedColumnId) => + optionalTimeScaling + ? (adjustTimeScaleOnOtherColumnChange(layer, thisColumnId, changedColumnId) as T) + : (layer.columns[thisColumnId] as T), getDefaultLabel: (column, indexPattern, columns) => - labelLookup(indexPattern.getFieldByName(column.sourceField)!.displayName, column), - buildColumn: ({ field, previousColumn }) => ({ - label: labelLookup(field.displayName, previousColumn), - dataType: 'number', - operationType: type, - sourceField: field.name, - isBucketed: false, - scale: 'ratio', - timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, - params: - previousColumn && previousColumn.dataType === 'number' ? previousColumn.params : undefined, - }), + labelLookup(getSafeName(column.sourceField, indexPattern), column), + buildColumn: ({ field, previousColumn }) => + ({ + label: labelLookup(field.displayName, previousColumn), + dataType: 'number', + operationType: type, + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, + params: + previousColumn && previousColumn.dataType === 'number' + ? previousColumn.params + : undefined, + } as T), onFieldChange: (oldColumn, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx new file mode 100644 index 0000000000000..c22eec62ea1ab --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { createMockedIndexPattern } from '../../mocks'; +import { percentileOperation } from './index'; +import { IndexPattern, IndexPatternLayer } from '../../types'; +import { PercentileIndexPatternColumn } from './percentile'; +import { EuiFieldNumber } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; +import { EuiFormRow } from '@elastic/eui'; + +const defaultProps = { + storage: {} as IStorageWrapper, + uiSettings: {} as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + data: dataPluginMock.createStartContract(), + http: {} as HttpSetup, + indexPattern: { + ...createMockedIndexPattern(), + hasRestrictions: false, + } as IndexPattern, +}; + +describe('percentile', () => { + let layer: IndexPatternLayer; + const InlineOptions = percentileOperation.paramEditor!; + + beforeEach(() => { + layer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + col2: { + label: '23rd percentile of a', + dataType: 'number', + isBucketed: false, + sourceField: 'a', + operationType: 'percentile', + params: { + percentile: 23, + }, + }, + }, + }; + }); + + describe('toEsAggsFn', () => { + it('should reflect params correctly', () => { + const percentileColumn = layer.columns.col2 as PercentileIndexPatternColumn; + const esAggsFn = percentileOperation.toEsAggsFn( + percentileColumn, + 'col1', + {} as IndexPattern, + layer + ); + expect(esAggsFn).toEqual( + expect.objectContaining({ + arguments: expect.objectContaining({ + percents: [23], + field: ['a'], + }), + }) + ); + }); + }); + + describe('onFieldChange', () => { + it('should change correctly to new field', () => { + const oldColumn: PercentileIndexPatternColumn = { + operationType: 'percentile', + sourceField: 'bytes', + label: '23rd percentile of bytes', + isBucketed: true, + dataType: 'number', + params: { + percentile: 23, + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('memory')!; + const column = percentileOperation.onFieldChange(oldColumn, newNumberField); + + expect(column).toEqual( + expect.objectContaining({ + dataType: 'number', + sourceField: 'memory', + params: expect.objectContaining({ + percentile: 23, + }), + }) + ); + expect(column.label).toContain('memory'); + }); + }); + + describe('buildColumn', () => { + it('should set default percentile', () => { + const indexPattern = createMockedIndexPattern(); + const bytesField = indexPattern.fields.find(({ name }) => name === 'bytes')!; + bytesField.displayName = 'test'; + const percentileColumn = percentileOperation.buildColumn({ + indexPattern, + field: bytesField, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }); + expect(percentileColumn.dataType).toEqual('number'); + expect(percentileColumn.params.percentile).toEqual(95); + expect(percentileColumn.label).toEqual('95th percentile of test'); + }); + }); + + describe('param editor', () => { + it('should render current percentile', () => { + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + + const input = instance.find('[data-test-subj="lns-indexPattern-percentile-input"]'); + + expect(input.prop('value')).toEqual('23'); + }); + + it('should update state on change', async () => { + jest.useFakeTimers(); + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + jest.runAllTimers(); + + const input = instance + .find('[data-test-subj="lns-indexPattern-percentile-input"]') + .find(EuiFieldNumber); + + await act(async () => { + input.prop('onChange')!({ target: { value: '27' } } as React.ChangeEvent); + }); + + instance.update(); + + jest.runAllTimers(); + + expect(updateLayerSpy).toHaveBeenCalledWith({ + ...layer, + columns: { + ...layer.columns, + col2: { + ...layer.columns.col2, + params: { + percentile: 27, + }, + label: '27th percentile of a', + }, + }, + }); + }); + + it('should not update on invalid input, but show invalid value locally', async () => { + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + jest.runAllTimers(); + + const input = instance + .find('[data-test-subj="lns-indexPattern-percentile-input"]') + .find(EuiFieldNumber); + + await act(async () => { + input.prop('onChange')!({ + target: { value: '12.12' }, + } as React.ChangeEvent); + }); + + instance.update(); + + jest.runAllTimers(); + + expect(updateLayerSpy).not.toHaveBeenCalled(); + + expect( + instance + .find('[data-test-subj="lns-indexPattern-percentile-form"]') + .find(EuiFormRow) + .prop('isInvalid') + ).toEqual(true); + expect( + instance + .find('[data-test-subj="lns-indexPattern-percentile-input"]') + .find(EuiFieldNumber) + .prop('value') + ).toEqual('12.12'); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx new file mode 100644 index 0000000000000..b381a0ecb664a --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { AggFunctionsMapping } from 'src/plugins/data/public'; +import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; +import { OperationDefinition } from './index'; +import { + getInvalidFieldMessage, + getSafeName, + isValidNumber, + useDebounceWithOptions, +} from './helpers'; +import { FieldBasedIndexPatternColumn } from './column_types'; + +export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'percentile'; + params: { + percentile: number; + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +function ofName(name: string, percentile: number) { + return i18n.translate('xpack.lens.indexPattern.percentileOf', { + defaultMessage: + '{percentile, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} percentile of {name}', + values: { name, percentile }, + }); +} + +const DEFAULT_PERCENTILE_VALUE = 95; + +export const percentileOperation: OperationDefinition = { + type: 'percentile', + displayName: i18n.translate('xpack.lens.indexPattern.percentile', { + defaultMessage: 'Percentile', + }), + input: 'field', + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { + if (fieldType === 'number' && aggregatable && !aggregationRestrictions) { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + } + }, + isTransferable: (column, newIndexPattern) => { + const newField = newIndexPattern.getFieldByName(column.sourceField); + + return Boolean( + newField && + newField.type === 'number' && + newField.aggregatable && + !newField.aggregationRestrictions + ); + }, + getDefaultLabel: (column, indexPattern, columns) => + ofName(getSafeName(column.sourceField, indexPattern), column.params.percentile), + buildColumn: ({ field, previousColumn, indexPattern }) => { + const existingFormat = + previousColumn?.params && 'format' in previousColumn?.params + ? previousColumn?.params?.format + : undefined; + const existingPercentileParam = + previousColumn?.operationType === 'percentile' && previousColumn?.params.percentile; + const newPercentileParam = existingPercentileParam || DEFAULT_PERCENTILE_VALUE; + return { + label: ofName(getSafeName(field.name, indexPattern), newPercentileParam), + dataType: 'number', + operationType: 'percentile', + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + params: { + format: existingFormat, + percentile: newPercentileParam, + }, + }; + }, + onFieldChange: (oldColumn, field) => { + return { + ...oldColumn, + label: ofName(field.displayName, oldColumn.params.percentile), + sourceField: field.name, + }; + }, + toEsAggsFn: (column, columnId, _indexPattern) => { + return buildExpressionFunction('aggPercentiles', { + id: columnId, + enabled: true, + schema: 'metric', + field: column.sourceField, + percents: [column.params.percentile], + }).toAst(); + }, + getEsAggsSuffix: (column) => { + const value = column.params.percentile; + return `.${value}`; + }, + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + paramEditor: function PercentileParamEditor({ + layer, + updateLayer, + currentColumn, + columnId, + indexPattern, + }) { + const [inputValue, setInputValue] = useState(String(currentColumn.params.percentile)); + + const inputValueAsNumber = Number(inputValue); + // an input is value if it's not an empty string, parses to a valid number, is between 0 and 100 (exclusive) + // and is an integer + const inputValueIsValid = isValidNumber(inputValue, true, 99, 1); + + useDebounceWithOptions( + () => { + if (!inputValueIsValid) return; + updateLayer({ + ...layer, + columns: { + ...layer.columns, + [columnId]: { + ...currentColumn, + label: currentColumn.customLabel + ? currentColumn.label + : ofName( + indexPattern.getFieldByName(currentColumn.sourceField)?.displayName || + currentColumn.sourceField, + inputValueAsNumber + ), + params: { + ...currentColumn.params, + percentile: inputValueAsNumber, + }, + }, + }, + }); + }, + { skipFirstRender: true }, + 256, + [inputValue] + ); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + const val = String(e.target.value); + setInputValue(val); + }, []); + return ( + + + + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index 9ab677bf68f62..420846f7fc801 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -22,7 +22,7 @@ import { keys, } from '@elastic/eui'; import { IFieldFormat } from '../../../../../../../../src/plugins/data/common'; -import { RangeTypeLens, isValidRange, isValidNumber } from './ranges'; +import { RangeTypeLens, isValidRange } from './ranges'; import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants'; import { NewBucketButton, @@ -30,7 +30,7 @@ import { DraggableBucketContainer, LabelInput, } from '../shared_components'; -import { useDebounceWithOptions } from '../helpers'; +import { isValidNumber, useDebounceWithOptions } from '../helpers'; const generateId = htmlIdGenerator(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index c2c52985c6cd2..987c8971aa310 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -142,7 +142,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toMatchInlineSnapshot(` Object { @@ -184,7 +185,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( @@ -203,7 +205,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( @@ -222,7 +225,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect((esAggsFn as { arguments: unknown }).arguments).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 2ba8f5febce5b..aa5cc8255a584 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -19,7 +19,7 @@ import { updateColumnParam } from '../../layer_helpers'; import { supportedFormats } from '../../../format_column'; import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; import { IndexPattern, IndexPatternField } from '../../../types'; -import { getInvalidFieldMessage } from '../helpers'; +import { getInvalidFieldMessage, isValidNumber } from '../helpers'; type RangeType = Omit; // Try to cover all possible serialized states for ranges @@ -52,10 +52,6 @@ export type UpdateParamsFnType = ( value: RangeColumnParams[K] ) => void; -// on initialization values can be null (from the Infinity serialization), so handle it correctly -// or they will be casted to 0 by the editor ( see #78867 ) -export const isValidNumber = (value: number | '' | null): value is number => - value != null && value !== '' && !isNaN(value) && isFinite(value); export const isRangeWithin = (range: RangeType): boolean => range.from <= range.to; const isFullRange = (range: RangeTypeLens): range is FullRangeTypeLens => isValidNumber(range.from) && isValidNumber(range.to); @@ -98,7 +94,10 @@ export const rangeOperation: OperationDefinition - indexPattern.getFieldByName(column.sourceField)!.displayName, + indexPattern.getFieldByName(column.sourceField)?.displayName ?? + i18n.translate('xpack.lens.indexPattern.missingFieldLabel', { + defaultMessage: 'Missing field', + }), buildColumn({ field }) { return { label: field.displayName, @@ -149,10 +148,10 @@ export const rangeOperation: OperationDefinition = { label: range.label }; // be careful with the fields to set on partial ranges if (isValidNumber(range.from)) { - partialRange.from = range.from; + partialRange.from = Number(range.from); } if (isValidNumber(range.to)) { - partialRange.to = range.to; + partialRange.to = Number(range.to); } return partialRange; }) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 888df40873a35..625084000fa93 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -18,23 +18,36 @@ import { } from '@elastic/eui'; import { AggFunctionsMapping } from '../../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public'; -import { IndexPatternColumn } from '../../../indexpattern'; import { updateColumnParam, isReferenced } from '../../layer_helpers'; import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesRangeInput } from './values_range_input'; -import { getInvalidFieldMessage } from '../helpers'; +import { getEsAggsSuffix, getInvalidFieldMessage } from '../helpers'; +import type { IndexPatternLayer } from '../../../types'; -function ofName(name: string) { +function ofName(name?: string) { return i18n.translate('xpack.lens.indexPattern.termsOf', { defaultMessage: 'Top values of {name}', - values: { name }, + values: { + name: + name ?? + i18n.translate('xpack.lens.indexPattern.missingFieldLabel', { + defaultMessage: 'Missing field', + }), + }, }); } -function isSortableByColumn(column: IndexPatternColumn) { - return !column.isBucketed && column.operationType !== 'last_value'; +function isSortableByColumn(layer: IndexPatternLayer, columnId: string) { + const column = layer.columns[columnId]; + return ( + column && + !column.isBucketed && + column.operationType !== 'last_value' && + !('references' in column) && + !isReferenced(layer, columnId) + ); } const DEFAULT_SIZE = 3; @@ -89,10 +102,7 @@ export const termsOperation: OperationDefinition - column && !isReferenced(layer, columnId) && isSortableByColumn(column) - ) + .filter(([columnId]) => isSortableByColumn(layer, columnId)) .map(([id]) => id)[0]; const previousBucketsLength = Object.values(layer.columns).filter( @@ -109,7 +119,10 @@ export const termsOperation: OperationDefinition { + toEsAggsFn: (column, columnId, _indexPattern, layer) => { return buildExpressionFunction('aggTerms', { id: columnId, enabled: true, schema: 'segment', field: column.sourceField, orderBy: - column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, + column.params.orderBy.type === 'alphabetical' + ? '_key' + : `${column.params.orderBy.columnId}${getEsAggsSuffix( + layer.columns[column.params.orderBy.columnId] + )}`, order: column.params.orderDirection, size: column.params.size, otherBucket: Boolean(column.params.otherBucket), @@ -138,7 +155,7 @@ export const termsOperation: OperationDefinition - ofName(indexPattern.getFieldByName(column.sourceField)!.displayName), + ofName(indexPattern.getFieldByName(column.sourceField)?.displayName), onFieldChange: (oldColumn, field) => { const newParams = { ...oldColumn.params }; if ('format' in newParams && field.type !== 'number') { @@ -152,11 +169,13 @@ export const termsOperation: OperationDefinition { + onOtherColumnChanged: (layer, thisColumnId, changedColumnId) => { + const columns = layer.columns; + const currentColumn = columns[thisColumnId] as TermsIndexPatternColumn; if (currentColumn.params.orderBy.type === 'column') { // check whether the column is still there and still a metric const columnSortedBy = columns[currentColumn.params.orderBy.columnId]; - if (!columnSortedBy || !isSortableByColumn(columnSortedBy)) { + if (!columnSortedBy || !isSortableByColumn(layer, changedColumnId)) { return { ...currentColumn, params: { @@ -194,7 +213,7 @@ export const termsOperation: OperationDefinition isSortableByColumn(column)) + .filter(([sortId]) => isSortableByColumn(layer, sortId)) .map(([sortId, column]) => { return { value: toValue({ type: 'column', columnId: sortId }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index eb78bb3ffebff..d60992bda2e2a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -65,7 +65,8 @@ describe('terms', () => { const esAggsFn = termsOperation.toEsAggsFn( { ...termsColumn, params: { ...termsColumn.params, otherBucket: true } }, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -87,7 +88,8 @@ describe('terms', () => { params: { ...termsColumn.params, otherBucket: false, missingBucket: true }, }, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -98,6 +100,45 @@ describe('terms', () => { }) ); }); + + it('should include esaggs suffix from other columns in orderby argument', () => { + const termsColumn = layer.columns.col1 as TermsIndexPatternColumn; + const esAggsFn = termsOperation.toEsAggsFn( + { + ...termsColumn, + params: { + ...termsColumn.params, + otherBucket: true, + orderBy: { type: 'column', columnId: 'abcde' }, + }, + }, + 'col1', + {} as IndexPattern, + { + ...layer, + columns: { + ...layer.columns, + abcde: { + dataType: 'number', + isBucketed: false, + operationType: 'percentile', + sourceField: 'abc', + label: '', + params: { + percentile: 12, + }, + }, + }, + } + ); + expect(esAggsFn).toEqual( + expect.objectContaining({ + arguments: expect.objectContaining({ + orderBy: ['abcde.12'], + }), + }) + ); + }); }); describe('onFieldChange', () => { @@ -402,15 +443,25 @@ describe('terms', () => { }, sourceField: 'category', }; - const updatedColumn = termsOperation.onOtherColumnChanged!(initialColumn, { - col1: { - label: 'Count', - dataType: 'number', - isBucketed: false, - sourceField: 'Records', - operationType: 'count', + const updatedColumn = termsOperation.onOtherColumnChanged!( + { + indexPatternId: '', + columnOrder: [], + columns: { + col2: initialColumn, + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, }, - }); + 'col2', + 'col1' + ); + expect(updatedColumn).toBe(initialColumn); }); @@ -429,18 +480,74 @@ describe('terms', () => { }, sourceField: 'category', }; - const updatedColumn = termsOperation.onOtherColumnChanged!(initialColumn, { - col1: { - label: 'Last Value', - dataType: 'number', - isBucketed: false, - sourceField: 'bytes', - operationType: 'last_value', - params: { - sortField: 'time', + const updatedColumn = termsOperation.onOtherColumnChanged!( + { + columns: { + col2: initialColumn, + col1: { + label: 'Last Value', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'last_value', + params: { + sortField: 'time', + }, + }, }, + columnOrder: [], + indexPatternId: '', }, - }); + 'col2', + 'col1' + ); + expect(updatedColumn.params).toEqual( + expect.objectContaining({ + orderBy: { type: 'alphabetical' }, + }) + ); + }); + + it('should switch to alphabetical ordering if metric is reference-based', () => { + const initialColumn: TermsIndexPatternColumn = { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }; + const updatedColumn = termsOperation.onOtherColumnChanged!( + { + columns: { + col2: initialColumn, + col1: { + label: 'Cumulative sum', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['referenced'], + }, + referenced: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'count', + sourceField: 'Records', + }, + }, + columnOrder: [], + indexPatternId: '', + }, + 'col2', + 'col1' + ); expect(updatedColumn.params).toEqual( expect.objectContaining({ orderBy: { type: 'alphabetical' }, @@ -451,20 +558,27 @@ describe('terms', () => { it('should switch to alphabetical ordering if there are no columns to order by', () => { const termsColumn = termsOperation.onOtherColumnChanged!( { - label: 'Top value of category', - dataType: 'string', - isBucketed: true, + columns: { + col2: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col1' }, - size: 3, - orderDirection: 'asc', + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, }, - sourceField: 'category', + columnOrder: [], + indexPatternId: '', }, - {} + 'col2', + 'col1' ); expect(termsColumn.params).toEqual( expect.objectContaining({ @@ -476,33 +590,39 @@ describe('terms', () => { it('should switch to alphabetical ordering if the order column is not a metric anymore', () => { const termsColumn = termsOperation.onOtherColumnChanged!( { - label: 'Top value of category', - dataType: 'string', - isBucketed: true, + columns: { + col2: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col1' }, - size: 3, - orderDirection: 'asc', - }, - sourceField: 'category', - }, - { - col1: { - label: 'Value of timestamp', - dataType: 'date', - isBucketed: true, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + col1: { + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, - // Private - operationType: 'date_histogram', - params: { - interval: 'w', + // Private + operationType: 'date_histogram', + params: { + interval: 'w', + }, + sourceField: 'timestamp', }, - sourceField: 'timestamp', }, - } + columnOrder: [], + indexPatternId: '', + }, + 'col2', + 'col1' ); expect(termsColumn.params).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts index 7123becf71b4d..079913347470a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts @@ -12,6 +12,7 @@ export { IndexPatternColumn, FieldBasedIndexPatternColumn, IncompleteColumn, + RequiredReference, } from './definitions'; export { createMockedReferenceOperation } from './mocks'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index bb09474798fd4..e0d9d864e5656 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -190,6 +190,44 @@ describe('state_helpers', () => { ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] })); }); + it('should insert a metric after buckets, but before references', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'h', + }, + }, + col3: { + label: 'Reference', + dataType: 'number', + isBucketed: false, + + operationType: 'cumulative_sum', + references: ['col2'], + }, + }, + }; + expect( + insertNewColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'count', + field: documentField, + }) + ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); + }); + it('should insert new buckets at the end of previous buckets', () => { const layer: IndexPatternLayer = { indexPatternId: '1', @@ -782,233 +820,608 @@ describe('state_helpers', () => { field: indexPattern.fields[2], // bytes field }); - expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith(termsColumn, { - col1: termsColumn, - col2: expect.objectContaining({ - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'avg', - sourceField: 'bytes', - }), - }); + expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( + { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: termsColumn, + col2: expect.objectContaining({ + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'avg', + }), + }, + incompleteColumns: {}, + }, + 'col1', + 'col2' + ); }); - it('should not wrap the previous operation when switching to reference', () => { - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Count', - customLabel: true, - dataType: 'number' as const, - isBucketed: false, - sourceField: 'Records', - operationType: 'count' as const, - }, + it('should execute adjustments for other columns when creating a reference', () => { + const termsColumn: TermsIndexPatternColumn = { + label: 'Top values of source', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'source', + params: { + orderBy: { type: 'column', columnId: 'willBeReference' }, + orderDirection: 'desc', + size: 5, }, }; - const result = replaceColumn({ - layer, + + replaceColumn({ + layer: { + indexPatternId: '1', + columnOrder: ['col1', 'willBeReference'], + columns: { + col1: termsColumn, + willBeReference: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }, indexPattern, - columnId: 'col1', - op: 'testReference' as OperationType, + columnId: 'willBeReference', + op: 'cumulative_sum', }); - expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( - expect.objectContaining({ - referenceIds: ['id1'], - }) - ); - expect(result.columns).toEqual( + expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( expect.objectContaining({ - col1: expect.objectContaining({ operationType: 'testReference' }), - }) + columns: { + col1: { + ...termsColumn, + params: { orderBy: { type: 'alphabetical' }, orderDirection: 'asc', size: 5 }, + }, + id1: expect.objectContaining({ + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }), + willBeReference: expect.objectContaining({ + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + }), + }, + incompleteColumns: {}, + }), + 'col1', + 'willBeReference' ); }); - it('should delete the previous references and reset to default values when going from reference to no-input', () => { - // @ts-expect-error this function is not valid - operationDefinitionMap.testReference.requiredReferences = [ - { - input: ['none'], - validateMetadata: () => true, - }, - ]; - const expectedCol = { - dataType: 'string' as const, - isBucketed: true, + describe('switching from non-reference to reference test cases', () => { + it('should wrap around the previous operation as a reference if possible (case new1)', () => { + const expectedColumn = { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, + }; - operationType: 'filters' as const, - params: { - // These filters are reset - filters: [{ input: { query: 'field: true', language: 'kuery' }, label: 'Custom label' }], - }, - }; - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - ...expectedCol, - label: 'Custom label', - customLabel: true, + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { col1: expectedColumn }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + op: 'testReference' as OperationType, + }); + + expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( + expect.objectContaining({ + referenceIds: ['id1'], + }) + ); + expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columns).toEqual( + expect.objectContaining({ + id1: expectedColumn, + col1: expect.any(Object), + }) + ); + }); + + it('should create a new no-input operation to use as reference (case new2)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['none'], + validateMetadata: () => true, }, - col2: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Avg', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'bytes', + operationType: 'avg' as const, + }, + }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + // @ts-expect-error + op: 'testReference', + }); - // @ts-expect-error not a valid type + expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columns).toEqual({ + id1: expect.objectContaining({ + operationType: 'filters', + }), + col1: expect.objectContaining({ operationType: 'testReference', - references: ['col1'], + }), + }); + }); + + it('should use the previous field, but select the best operation, when creating a reference (case new3)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['field'], + validateMetadata: () => true, + specificOperations: ['cardinality', 'sum', 'avg'], // this order is ignored }, - }, - }; - expect( - replaceColumn({ + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Max', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'bytes', + operationType: 'max' as const, + }, + }, + }; + const result = replaceColumn({ layer, indexPattern, - columnId: 'col2', - op: 'filters', - }) - ).toEqual( - expect.objectContaining({ - columnOrder: ['col2'], + columnId: 'col1', + // @ts-expect-error test only + op: 'testReference', + }); + + expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columns).toEqual({ + id1: expect.objectContaining({ + operationType: 'avg', + }), + col1: expect.objectContaining({ + operationType: 'testReference', + }), + }); + }); + + it('should ignore previous field and previous operation, but set incomplete operation if known (case new4)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['field'], + validateMetadata: () => true, + specificOperations: ['cardinality'], + }, + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], columns: { - col2: { - ...expectedCol, - label: 'Filters', - scale: 'ordinal', // added in buildColumn - params: { - filters: [{ input: { query: '', language: 'kuery' }, label: '' }], - }, + col1: { + label: 'Count', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, }, }, - }) - ); + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + // @ts-expect-error + op: 'testReference', + }); + + expect(result.incompleteColumns).toEqual({ + id1: { operationType: 'cardinality' }, + }); + expect(result.columns).toEqual({ + col1: expect.objectContaining({ + operationType: 'testReference', + }), + }); + }); + + it('should leave an empty reference if all the other cases fail (case new6)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['field'], + validateMetadata: () => false, + specificOperations: [], + }, + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Count', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, + }, + }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + // @ts-expect-error + op: 'testReference', + }); + + expect(result.incompleteColumns).toEqual({}); + expect(result.columns).toEqual({ + col1: expect.objectContaining({ + operationType: 'testReference', + references: ['id1'], + }), + }); + }); }); - it('should delete the inner references when switching away from reference to field-based operation', () => { - const expectedCol = { - label: 'Count of records', - dataType: 'number' as const, - isBucketed: false, + describe('switching from reference to reference test cases', () => { + beforeEach(() => { + operationDefinitionMap.secondTest = { + input: 'fullReference', + displayName: 'Reference test 2', + // @ts-expect-error this type is not statically available + type: 'secondTest', + requiredReferences: [ + { + // Any numeric metric that isn't also a reference + input: ['none', 'field'], + validateMetadata: (meta: OperationMetadata) => + meta.dataType === 'number' && !meta.isBucketed, + }, + ], + // @ts-expect-error don't want to define valid arguments + buildColumn: jest.fn((args) => { + return { + label: 'Test reference', + isBucketed: false, + dataType: 'number', + + operationType: 'secondTest', + references: args.referenceIds, + }; + }), + isTransferable: jest.fn(), + toExpression: jest.fn().mockReturnValue([]), + getPossibleOperation: jest + .fn() + .mockReturnValue({ dataType: 'number', isBucketed: false }), + getDefaultLabel: jest.fn().mockReturnValue('Test reference'), + }; + }); + + afterEach(() => { + delete operationDefinitionMap.secondTest; + }); + + it('should use existing references, delete invalid, when switching from one reference to another (case ref1)', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['ref1', 'invalid', 'output'], + columns: { + ref1: { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }, + invalid: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: [], + }, + output: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['ref1', 'invalid'], + }, + }, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'output', + // @ts-expect-error not statically available + op: 'secondTest', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['ref1', 'output'], + columns: { + ref1: layer.columns.ref1, + output: expect.objectContaining({ references: ['ref1'] }), + }, + incompleteColumns: {}, + }) + ); + }); + + it('should modify a copied object, not the original layer', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['ref1', 'invalid', 'output'], + columns: { + ref1: { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }, + invalid: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, - operationType: 'count' as const, - sourceField: 'Records', - }; - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: expectedCol, - col2: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, + // @ts-expect-error not a valid type + operationType: 'testReference', + references: [], + }, + output: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, - // @ts-expect-error not a valid type - operationType: 'testReference', - references: ['col1'], + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['ref1', 'invalid'], + }, }, - }, - }; - expect( + }; replaceColumn({ layer, indexPattern, - columnId: 'col2', - op: 'count', - field: documentField, - }) - ).toEqual( - expect.objectContaining({ - columnOrder: ['col2'], + columnId: 'output', + // @ts-expect-error not statically available + op: 'secondTest', + }); + expect(layer.columns.output).toEqual( + expect.objectContaining({ references: ['ref1', 'invalid'] }) + ); + }); + + it('should transition by using the field from the previous reference if nothing else works (case new5)', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['fieldReused', 'output'], columns: { - col2: expect.objectContaining(expectedCol), + fieldReused: { + label: 'Date histogram', + dataType: 'date' as const, + isBucketed: true, + operationType: 'date_histogram' as const, + sourceField: 'timestamp', + params: { interval: 'auto' }, + }, + output: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['fieldReused'], + }, }, - }) - ); + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'output', + // @ts-expect-error not statically available + op: 'secondTest', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['id1', 'output'], + columns: { + id1: expect.objectContaining({ + sourceField: 'timestamp', + operationType: 'cardinality', + }), + output: expect.objectContaining({ references: ['id1'] }), + }, + incompleteColumns: {}, + }) + ); + }); }); - it('should reset when switching from one reference to another', () => { - operationDefinitionMap.secondTest = { - input: 'fullReference', - displayName: 'Reference test 2', - // @ts-expect-error this type is not statically available - type: 'secondTest', - requiredReferences: [ + describe('switching from reference to non-reference', () => { + it('should promote the inner references when switching away from reference to no-input (case a1)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ { - // Any numeric metric that isn't also a reference - input: ['none', 'field'], - validateMetadata: (meta: OperationMetadata) => - meta.dataType === 'number' && !meta.isBucketed, + input: ['none'], + validateMetadata: () => true, }, - ], - // @ts-expect-error don't want to define valid arguments - buildColumn: jest.fn((args) => { - return { - label: 'Test reference', - isBucketed: false, - dataType: 'number', + ]; + const expectedCol = { + label: 'Custom label', + customLabel: true, + dataType: 'string' as const, + isBucketed: true, - operationType: 'secondTest', - references: args.referenceIds, - }; - }), - isTransferable: jest.fn(), - toExpression: jest.fn().mockReturnValue([]), - getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), - getDefaultLabel: () => 'Test reference', - }; + operationType: 'filters' as const, + params: { + // These filters are reset + filters: [ + { input: { query: 'field: true', language: 'kuery' }, label: 'Custom label' }, + ], + }, + }; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: expectedCol, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - label: 'Count', - customLabel: true, - dataType: 'number' as const, - isBucketed: false, + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'filters', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2'], + columns: { + col2: expectedCol, + }, + }) + ); + }); - operationType: 'count' as const, - sourceField: 'Records', + it('should promote the inner references when switching away from reference to field-based operation (case a2)', () => { + const expectedCol = { + label: 'Count of records', + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: expectedCol, + col2: { + label: 'Default label', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, }, - col2: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'count', + field: documentField, + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2'], + columns: { + col2: expect.objectContaining(expectedCol), + }, + }) + ); + }); - // @ts-expect-error not a valid type - operationType: 'testReference', - references: ['col1'], + it('should promote only the field when going from reference to field-based operation (case a3)', () => { + const expectedColumn = { + dataType: 'number' as const, + isBucketed: false, + sourceField: 'bytes', + operationType: 'avg' as const, + }; + + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['metric', 'ref'], + columns: { + metric: { ...expectedColumn, label: 'Avg', customLabel: true }, + ref: { + label: 'Reference', + dataType: 'number', + isBucketed: false, + operationType: 'derivative', + references: ['metric'], + }, }, - }, - }; - expect( - replaceColumn({ + }; + const result = replaceColumn({ layer, indexPattern, - columnId: 'col2', - // @ts-expect-error not statically available - op: 'secondTest', - }) - ).toEqual( - expect.objectContaining({ - columnOrder: ['col2'], - columns: { - col2: expect.objectContaining({ references: ['id1'] }), - }, - incompleteColumns: {}, - }) - ); + columnId: 'ref', + op: 'sum', + }); - delete operationDefinitionMap.secondTest; + expect(result.columnOrder).toEqual(['ref']); + expect(result.columns).toEqual( + expect.objectContaining({ + ref: expect.objectContaining({ ...expectedColumn, operationType: 'sum' }), + }) + ); + }); }); it('should allow making a replacement on an operation that is being referenced, even if it ends up invalid', () => { @@ -1081,6 +1494,7 @@ describe('state_helpers', () => { }, }, columnId: 'col1', + indexPattern, }) ).toEqual({ indexPatternId: '1', @@ -1126,6 +1540,7 @@ describe('state_helpers', () => { }, }, columnId: 'col2', + indexPattern, }) ).toEqual({ indexPatternId: '1', @@ -1176,11 +1591,14 @@ describe('state_helpers', () => { }, }, columnId: 'col2', + indexPattern, }); - expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith(termsColumn, { - col1: termsColumn, - }); + expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( + { indexPatternId: '1', columnOrder: ['col1', 'col2'], columns: { col1: termsColumn } }, + 'col1', + 'col2' + ); }); it('should delete the column and all of its references', () => { @@ -1207,11 +1625,57 @@ describe('state_helpers', () => { }, }, }; - expect(deleteColumn({ layer, columnId: 'col2' })).toEqual( + expect(deleteColumn({ layer, columnId: 'col2', indexPattern })).toEqual( expect.objectContaining({ columnOrder: [], columns: {} }) ); }); + it('should update the labels when deleting columns', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + operationType: 'count', + sourceField: 'Records', + }, + col2: { + label: 'Changed label', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + deleteColumn({ layer, columnId: 'col1', indexPattern }); + expect(operationDefinitionMap.testReference.getDefaultLabel).toHaveBeenCalledWith( + { + label: 'Changed label', + dataType: 'number', + isBucketed: false, + operationType: 'testReference', + references: ['col1'], + }, + indexPattern, + { + col2: { + label: 'Default label', + dataType: 'number', + isBucketed: false, + operationType: 'testReference', + references: ['col1'], + }, + } + ); + }); + it('should recursively delete references', () => { const layer: IndexPatternLayer = { indexPatternId: '1', @@ -1245,7 +1709,7 @@ describe('state_helpers', () => { }, }, }; - expect(deleteColumn({ layer, columnId: 'col3' })).toEqual( + expect(deleteColumn({ layer, columnId: 'col3', indexPattern })).toEqual( expect.objectContaining({ columnOrder: [], columns: {} }) ); }); @@ -1680,63 +2144,34 @@ describe('state_helpers', () => { }); describe('getErrorMessages', () => { - it('should collect errors from the operation definitions', () => { + it('should collect errors from metric-type operation definitions', () => { const mock = jest.fn().mockReturnValue(['error 1']); - operationDefinitionMap.testReference.getErrorMessage = mock; + operationDefinitionMap.avg.getErrorMessage = mock; const errors = getErrorMessages({ indexPatternId: '1', columnOrder: [], columns: { - col1: - // @ts-expect-error not statically analyzed - { operationType: 'testReference', references: [] }, + // @ts-expect-error invalid column + col1: { operationType: 'avg' }, }, }); expect(mock).toHaveBeenCalled(); expect(errors).toHaveLength(1); }); - it('should identify missing references', () => { + it('should collect errors from reference-type operation definitions', () => { + const mock = jest.fn().mockReturnValue(['error 1']); + operationDefinitionMap.testReference.getErrorMessage = mock; const errors = getErrorMessages({ indexPatternId: '1', columnOrder: [], columns: { col1: - // @ts-expect-error not statically analyzed yet - { operationType: 'testReference', references: ['ref1', 'ref2'] }, - }, - }); - expect(errors).toHaveLength(2); - }); - - it('should identify references that are no longer valid', () => { - // There is only one operation with `none` as the input type - // @ts-expect-error this function is not valid - operationDefinitionMap.testReference.requiredReferences = [ - { - input: ['none'], - validateMetadata: () => true, - }, - ]; - - const errors = getErrorMessages({ - indexPatternId: '1', - columnOrder: [], - columns: { - // @ts-expect-error incomplete operation - ref1: { - dataType: 'string', - isBucketed: true, - operationType: 'terms', - }, - col1: { - label: '', - references: ['ref1'], - // @ts-expect-error tests only - operationType: 'testReference', - }, + // @ts-expect-error not statically analyzed + { operationType: 'testReference', references: [] }, }, }); + expect(mock).toHaveBeenCalled(); expect(errors).toHaveLength(1); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 1619ad907fffc..21fc36d7418ba 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -5,7 +5,7 @@ */ import _, { partition } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import type { OperationMetadata } from '../../types'; import { operationDefinitionMap, operationDefinitions, @@ -60,11 +60,11 @@ export function insertNewColumn({ } const possibleOperation = operationDefinition.getPossibleOperation(); const isBucketed = Boolean(possibleOperation.isBucketed); - if (isBucketed) { - return addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId); - } else { - return addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId); - } + const addOperationFn = isBucketed ? addBucket : addMetric; + return updateDefaultLabels( + addOperationFn(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId), + indexPattern + ); } if (operationDefinition.input === 'fullReference') { @@ -73,11 +73,8 @@ export function insertNewColumn({ } let tempLayer = { ...layer }; const referenceIds = operationDefinition.requiredReferences.map((validation) => { - // TODO: This logic is too simple because it's not using fields. Once we have - // access to the operationSupportMatrix, we should validate the metadata against - // the possible fields const validOperations = Object.values(operationDefinitionMap).filter(({ type }) => - isOperationAllowedAsReference({ validation, operationType: type }) + isOperationAllowedAsReference({ validation, operationType: type, indexPattern }) ); if (!validOperations.length) { @@ -122,29 +119,23 @@ export function insertNewColumn({ return newId; }); - const possibleOperation = operationDefinition.getPossibleOperation(); - const isBucketed = Boolean(possibleOperation.isBucketed); - if (isBucketed) { - return addBucket( - tempLayer, - operationDefinition.buildColumn({ - ...baseOptions, - layer: tempLayer, - referenceIds, - }), - columnId + const possibleOperation = operationDefinition.getPossibleOperation(indexPattern); + if (!possibleOperation) { + throw new Error( + `Can't create operation ${op} because it's incompatible with the index pattern` ); - } else { - return addMetric( + } + const isBucketed = Boolean(possibleOperation.isBucketed); + + const addOperationFn = isBucketed ? addBucket : addMetric; + return updateDefaultLabels( + addOperationFn( tempLayer, - operationDefinition.buildColumn({ - ...baseOptions, - layer: tempLayer, - referenceIds, - }), + operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, referenceIds }), columnId - ); - } + ), + indexPattern + ); } const invalidFieldName = (layer.incompleteColumns ?? {})[columnId]?.sourceField; @@ -159,16 +150,22 @@ export function insertNewColumn({ } const isBucketed = Boolean(possibleOperation.isBucketed); if (isBucketed) { - return addBucket( - layer, - operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), - columnId + return updateDefaultLabels( + addBucket( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ), + indexPattern ); } else { - return addMetric( - layer, - operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), - columnId + return updateDefaultLabels( + addMetric( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ), + indexPattern ); } } else if (!field) { @@ -193,19 +190,15 @@ export function insertNewColumn({ }; } const isBucketed = Boolean(possibleOperation.isBucketed); - if (isBucketed) { - return addBucket( - layer, - operationDefinition.buildColumn({ ...baseOptions, layer, field }), - columnId - ); - } else { - return addMetric( + const addOperationFn = isBucketed ? addBucket : addMetric; + return updateDefaultLabels( + addOperationFn( layer, operationDefinition.buildColumn({ ...baseOptions, layer, field }), columnId - ); - } + ), + indexPattern + ); } export function replaceColumn({ @@ -239,44 +232,101 @@ export function replaceColumn({ tempLayer = resetIncomplete(tempLayer, columnId); - if (previousDefinition.input === 'fullReference') { - (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { - tempLayer = deleteColumn({ layer: tempLayer, columnId: id }); + if (operationDefinition.input === 'fullReference') { + return applyReferenceTransition({ + layer: tempLayer, + columnId, + previousColumn, + op, + indexPattern, }); } - if (operationDefinition.input === 'fullReference') { - const referenceIds = operationDefinition.requiredReferences.map(() => generateId()); - - const newColumns = { - ...tempLayer.columns, - [columnId]: operationDefinition.buildColumn({ - ...baseOptions, - layer: tempLayer, - referenceIds, - previousColumn, - }), - }; - return { - ...tempLayer, - columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), - columns: newColumns, - }; + // Makes common inferences about what the user meant when switching away from a reference: + // 1. Switching from "Differences of max" to "max" will promote as-is + // 2. Switching from "Differences of avg of bytes" to "max" will keep the field, but change operation + if ( + previousDefinition.input === 'fullReference' && + (previousColumn as ReferenceBasedIndexPatternColumn).references.length === 1 + ) { + const previousReferenceId = (previousColumn as ReferenceBasedIndexPatternColumn) + .references[0]; + const referenceColumn = layer.columns[previousReferenceId]; + if (referenceColumn) { + const referencedOperation = operationDefinitionMap[referenceColumn.operationType]; + + if (referencedOperation.type === op) { + // Unit tests are labelled as case a1, case a2 + tempLayer = deleteColumn({ + layer: tempLayer, + columnId: previousReferenceId, + indexPattern, + }); + + tempLayer = { + ...tempLayer, + columns: { + ...tempLayer.columns, + [columnId]: copyCustomLabel({ ...referenceColumn }, previousColumn), + }, + }; + return updateDefaultLabels( + { + ...tempLayer, + columnOrder: getColumnOrder(tempLayer), + columns: adjustColumnReferencesForChangedColumn(tempLayer, columnId), + }, + indexPattern + ); + } else if ( + !field && + 'sourceField' in referenceColumn && + referencedOperation.input === 'field' && + operationDefinition.input === 'field' + ) { + // Unit test is case a3 + const matchedField = indexPattern.getFieldByName(referenceColumn.sourceField); + if (matchedField && operationDefinition.getPossibleOperationForField(matchedField)) { + field = matchedField; + } + } + } + } + + // This logic comes after the transitions because they need to look at previous columns + if (previousDefinition.input === 'fullReference') { + (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { + tempLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern }); + }); } if (operationDefinition.input === 'none') { let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer }); - newColumn = adjustLabel(newColumn, previousColumn); + newColumn = copyCustomLabel(newColumn, previousColumn); + + const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; + return updateDefaultLabels( + { + ...tempLayer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, columnId), + }, + indexPattern + ); + } - const newColumns = { ...tempLayer.columns, [columnId]: newColumn }; + if (!field) { return { ...tempLayer, - columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), - columns: adjustColumnReferencesForChangedColumn(newColumns, columnId), + incompleteColumns: { + ...(tempLayer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, }; } - if (!field) { + const validOperation = operationDefinition.getPossibleOperationForField(field); + if (!validOperation) { return { ...tempLayer, incompleteColumns: { @@ -285,16 +335,18 @@ export function replaceColumn({ }, }; } - let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); - newColumn = adjustLabel(newColumn, previousColumn); + newColumn = copyCustomLabel(newColumn, previousColumn); - const newColumns = { ...tempLayer.columns, [columnId]: newColumn }; - return { - ...tempLayer, - columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), - columns: adjustColumnReferencesForChangedColumn(newColumns, columnId), - }; + const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; + return updateDefaultLabels( + { + ...tempLayer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, columnId), + }, + indexPattern + ); } else if ( operationDefinition.input === 'field' && field && @@ -302,26 +354,274 @@ export function replaceColumn({ previousColumn.sourceField !== field.name ) { // Same operation, new field - const newColumn = operationDefinition.onFieldChange(previousColumn, field); + const newColumn = copyCustomLabel( + operationDefinition.onFieldChange(previousColumn, field), + previousColumn + ); - const newColumns = { ...layer.columns, [columnId]: adjustLabel(newColumn, previousColumn) }; + const newLayer = resetIncomplete( + { ...layer, columns: { ...layer.columns, [columnId]: newColumn } }, + columnId + ); return { - ...layer, - columnOrder: getColumnOrder({ ...layer, columns: newColumns }), - columns: adjustColumnReferencesForChangedColumn(newColumns, columnId), + ...newLayer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, columnId), }; } else { throw new Error('nothing changed'); } } -function adjustLabel(newColumn: IndexPatternColumn, previousColumn: IndexPatternColumn) { +export function canTransition({ + layer, + columnId, + op, + field, + indexPattern, + filterOperations, +}: ColumnChange & { + filterOperations: (meta: OperationMetadata) => boolean; +}): boolean { + const previousColumn = layer.columns[columnId]; + if (!previousColumn) { + return true; + } + + if (previousColumn.operationType === op) { + return true; + } + + try { + const newLayer = replaceColumn({ layer, columnId, op, field, indexPattern }); + const newDefinition = operationDefinitionMap[op]; + const newColumn = newLayer.columns[columnId]; + return ( + Boolean(newColumn) && + !newLayer.incompleteColumns?.[columnId] && + filterOperations(newColumn) && + !newDefinition.getErrorMessage?.(newLayer, columnId, indexPattern) + ); + } catch (e) { + return false; + } +} + +/** + * Function to transition to a fullReference from any different operation. + * It is always possible to transition to a fullReference, but there are multiple + * passes needed to copy all the previous state. These are the passes in priority + * order, each of which has a unit test: + * + * 1. Case ref1: referenced columns are an exact match + * Side effect: Modifies the reference list directly + * 2. Case new1: the previous column is an exact match. + * Side effect: Deletes and then inserts the previous column + * 3. Case new2: the reference supports `none` inputs, like filters. not visible in the UI. + * Side effect: Inserts a new column + * 4. Case new3, new4: Fuzzy matching on the previous field + * Side effect: Inserts a new column, or an incomplete column + * 5. Fuzzy matching based on the previous references (case new6) + * Side effect: Inserts a new column, or an incomplete column + * Side effect: Modifies the reference list directly + * 6. Case new6: Fall back by generating the column with empty references + */ +function applyReferenceTransition({ + layer, + columnId, + previousColumn, + op, + indexPattern, +}: { + layer: IndexPatternLayer; + columnId: string; + previousColumn: IndexPatternColumn; + op: OperationType; + indexPattern: IndexPattern; +}): IndexPatternLayer { + const operationDefinition = operationDefinitionMap[op]; + + if (operationDefinition.input !== 'fullReference') { + throw new Error(`Requirements for transitioning are not met`); + } + + let hasExactMatch = false; + let hasFieldMatch = false; + + const unusedReferencesQueue = + 'references' in previousColumn + ? [...(previousColumn as ReferenceBasedIndexPatternColumn).references] + : []; + + const referenceIds = operationDefinition.requiredReferences.map((validation) => { + const newId = generateId(); + + // First priority is to use any references that can be kept (case ref1) + if (unusedReferencesQueue.length) { + const otherColumn = layer.columns[unusedReferencesQueue[0]]; + if (isColumnValidAsReference({ validation, column: otherColumn })) { + return unusedReferencesQueue.shift()!; + } + } + + // Second priority is to wrap around the previous column (case new1) + if (!hasExactMatch && isColumnValidAsReference({ validation, column: previousColumn })) { + hasExactMatch = true; + + const newLayer = { ...layer, columns: { ...layer.columns, [newId]: { ...previousColumn } } }; + layer = { + ...layer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, newId), + }; + return newId; + } + + // Look for any fieldless operations that can be inserted directly (case new2) + if (validation.input.includes('none')) { + const validOperations = operationDefinitions.filter((def) => { + if (def.input !== 'none') return; + return isOperationAllowedAsReference({ + validation, + operationType: def.type, + indexPattern, + }); + }); + + if (validOperations.length === 1) { + layer = insertNewColumn({ + layer, + columnId: newId, + op: validOperations[0].type, + indexPattern, + }); + return newId; + } + } + + // Try to reuse the previous field by finding a possible operation. Because we've alredy + // checked for an exact operation match, this is guaranteed to be different from previousColumn + if (!hasFieldMatch && 'sourceField' in previousColumn && validation.input.includes('field')) { + const defIgnoringfield = operationDefinitions + .filter( + (def) => + def.input === 'field' && + isOperationAllowedAsReference({ validation, operationType: def.type, indexPattern }) + ) + .sort(getSortScoreByPriority); + + // No exact match found, so let's determine that the current field can be reused + const defWithField = defIgnoringfield.filter((def) => { + const previousField = indexPattern.getFieldByName(previousColumn.sourceField); + if (!previousField) return; + return isOperationAllowedAsReference({ + validation, + operationType: def.type, + field: previousField, + indexPattern, + }); + }); + + if (defWithField.length > 0) { + // Found the best match that keeps the field (case new3) + hasFieldMatch = true; + layer = insertNewColumn({ + layer, + columnId: newId, + op: defWithField[0].type, + indexPattern, + field: indexPattern.getFieldByName(previousColumn.sourceField), + }); + return newId; + } else if (defIgnoringfield.length === 1) { + // Can't use the field, but there is an exact match on the operation (case new4) + hasFieldMatch = true; + layer = { + ...layer, + incompleteColumns: { + ...layer.incompleteColumns, + [newId]: { operationType: defIgnoringfield[0].type }, + }, + }; + return newId; + } + } + + // Look for field-based references that we can use to assign a new field-based operation from (case new5) + if (unusedReferencesQueue.length) { + const otherColumn = layer.columns[unusedReferencesQueue[0]]; + if (otherColumn && 'sourceField' in otherColumn && validation.input.includes('field')) { + const previousField = indexPattern.getFieldByName(otherColumn.sourceField); + if (previousField) { + const defWithField = operationDefinitions + .filter( + (def) => + def.input === 'field' && + isOperationAllowedAsReference({ + validation, + operationType: def.type, + field: previousField, + indexPattern, + }) + ) + .sort(getSortScoreByPriority); + + if (defWithField.length > 0) { + layer = insertNewColumn({ + layer, + columnId: newId, + op: defWithField[0].type, + indexPattern, + field: previousField, + }); + return newId; + } + } + } + } + + // The reference is too ambiguous at this point, but instead of throwing an error (case new6) + return newId; + }); + + if (unusedReferencesQueue.length) { + unusedReferencesQueue.forEach((id: string) => { + layer = deleteColumn({ + layer, + columnId: id, + indexPattern, + }); + }); + } + + layer = { + ...layer, + columns: { + ...layer.columns, + [columnId]: operationDefinition.buildColumn({ + indexPattern, + layer, + referenceIds, + previousColumn, + }), + }, + }; + return updateDefaultLabels( + { + ...layer, + columnOrder: getColumnOrder(layer), + columns: adjustColumnReferencesForChangedColumn(layer, columnId), + }, + indexPattern + ); +} + +function copyCustomLabel(newColumn: IndexPatternColumn, previousColumn: IndexPatternColumn) { const adjustedColumn = { ...newColumn }; if (previousColumn.customLabel) { adjustedColumn.customLabel = true; adjustedColumn.label = previousColumn.label; } - return adjustedColumn; } @@ -370,7 +670,6 @@ function addMetric( ...layer.columns, [addedColumnId]: column, }, - columnOrder: [...layer.columnOrder, addedColumnId], }; return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } @@ -409,17 +708,18 @@ export function updateColumnParam({ }; } -function adjustColumnReferencesForChangedColumn( - columns: Record, - columnId: string -) { - const newColumns = { ...columns }; +function adjustColumnReferencesForChangedColumn(layer: IndexPatternLayer, changedColumnId: string) { + const newColumns = { ...layer.columns }; Object.keys(newColumns).forEach((currentColumnId) => { - if (currentColumnId !== columnId) { + if (currentColumnId !== changedColumnId) { const currentColumn = newColumns[currentColumnId]; const operationDefinition = operationDefinitionMap[currentColumn.operationType]; newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged - ? operationDefinition.onOtherColumnChanged(currentColumn, newColumns) + ? operationDefinition.onOtherColumnChanged( + { ...layer, columns: newColumns }, + currentColumnId, + changedColumnId + ) : currentColumn; } }); @@ -429,9 +729,11 @@ function adjustColumnReferencesForChangedColumn( export function deleteColumn({ layer, columnId, + indexPattern, }: { layer: IndexPatternLayer; columnId: string; + indexPattern: IndexPattern; }): IndexPatternLayer { const column = layer.columns[columnId]; if (!column) { @@ -451,17 +753,27 @@ export function deleteColumn({ let newLayer = { ...layer, - columns: adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId), + columns: adjustColumnReferencesForChangedColumn( + { ...layer, columns: hypotheticalColumns }, + columnId + ), }; extraDeletions.forEach((id) => { - newLayer = deleteColumn({ layer: newLayer, columnId: id }); + newLayer = deleteColumn({ layer: newLayer, columnId: id, indexPattern }); }); const newIncomplete = { ...(newLayer.incompleteColumns || {}) }; delete newIncomplete[columnId]; - return { ...newLayer, columnOrder: getColumnOrder(newLayer), incompleteColumns: newIncomplete }; + return updateDefaultLabels( + { + ...newLayer, + columnOrder: getColumnOrder(newLayer), + incompleteColumns: newIncomplete, + }, + indexPattern + ); } // Derives column order from column object, respects existing columnOrder @@ -482,7 +794,7 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { const [direct, referenceBased] = _.partition( entries, - ([id, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference' + ([, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference' ); // If a reference has another reference as input, put it last in sort order referenceBased.sort(([idA, a], [idB, b]) => { @@ -503,7 +815,7 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { } // Splits existing columnOrder into the three categories -function getExistingColumnGroups(layer: IndexPatternLayer): [string[], string[], string[]] { +export function getExistingColumnGroups(layer: IndexPatternLayer): [string[], string[], string[]] { const [direct, referenced] = partition( layer.columnOrder, (columnId) => layer.columns[columnId] && !('references' in layer.columns[columnId]) @@ -553,44 +865,9 @@ export function getErrorMessages(layer: IndexPatternLayer): string[] | undefined Object.entries(layer.columns).forEach(([columnId, column]) => { const def = operationDefinitionMap[column.operationType]; - if (def.input === 'fullReference' && def.getErrorMessage) { + if (def.getErrorMessage) { errors.push(...(def.getErrorMessage(layer, columnId) ?? [])); } - - if ('references' in column) { - column.references.forEach((referenceId, index) => { - if (!layer.columns[referenceId]) { - errors.push( - i18n.translate('xpack.lens.indexPattern.missingReferenceError', { - defaultMessage: 'Dimension {dimensionLabel} is incomplete', - values: { - dimensionLabel: column.label, - }, - }) - ); - } else { - const referenceColumn = layer.columns[referenceId]!; - const requirements = - // @ts-expect-error not statically analyzed - operationDefinitionMap[column.operationType].requiredReferences[index]; - const isValid = isColumnValidAsReference({ - validation: requirements, - column: referenceColumn, - }); - - if (!isValid) { - errors.push( - i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', { - defaultMessage: 'Dimension {dimensionLabel} does not have a valid configuration', - values: { - dimensionLabel: column.label, - }, - }) - ); - } - } - }); - } }); return errors.length ? errors : undefined; @@ -603,30 +880,15 @@ export function isReferenced(layer: IndexPatternLayer, columnId: string): boolea return allReferences.includes(columnId); } -function isColumnValidAsReference({ - column, - validation, -}: { - column: IndexPatternColumn; - validation: RequiredReference; -}): boolean { - if (!column) return false; - const operationType = column.operationType; - const operationDefinition = operationDefinitionMap[operationType]; - return ( - validation.input.includes(operationDefinition.input) && - (!validation.specificOperations || validation.specificOperations.includes(operationType)) && - validation.validateMetadata(column) - ); -} - -function isOperationAllowedAsReference({ +export function isOperationAllowedAsReference({ operationType, validation, field, + indexPattern, }: { operationType: OperationType; validation: RequiredReference; + indexPattern: IndexPattern; field?: IndexPatternField; }): boolean { const operationDefinition = operationDefinitionMap[operationType]; @@ -635,9 +897,12 @@ function isOperationAllowedAsReference({ if (field && operationDefinition.input === 'field') { const metadata = operationDefinition.getPossibleOperationForField(field); hasValidMetadata = Boolean(metadata) && validation.validateMetadata(metadata!); - } else if (operationDefinition.input !== 'field') { + } else if (operationDefinition.input === 'none') { const metadata = operationDefinition.getPossibleOperation(); hasValidMetadata = Boolean(metadata) && validation.validateMetadata(metadata!); + } else if (operationDefinition.input === 'fullReference') { + const metadata = operationDefinition.getPossibleOperation(indexPattern); + hasValidMetadata = Boolean(metadata) && validation.validateMetadata(metadata!); } else { // TODO: How can we validate the metadata without a specific field? } @@ -648,8 +913,48 @@ function isOperationAllowedAsReference({ ); } +// Labels need to be updated when columns are added because reference-based column labels +// are sometimes copied into the parents +function updateDefaultLabels( + layer: IndexPatternLayer, + indexPattern: IndexPattern +): IndexPatternLayer { + const copiedColumns = { ...layer.columns }; + layer.columnOrder.forEach((id) => { + const col = copiedColumns[id]; + if (!col.customLabel) { + copiedColumns[id] = { + ...col, + label: operationDefinitionMap[col.operationType].getDefaultLabel( + col, + indexPattern, + copiedColumns + ), + }; + } + }); + return { ...layer, columns: copiedColumns }; +} + export function resetIncomplete(layer: IndexPatternLayer, columnId: string): IndexPatternLayer { const incompleteColumns = { ...(layer.incompleteColumns ?? {}) }; delete incompleteColumns[columnId]; return { ...layer, incompleteColumns }; } + +export function isColumnValidAsReference({ + column, + validation, +}: { + column: IndexPatternColumn; + validation: RequiredReference; +}): boolean { + if (!column) return false; + const operationType = column.operationType; + const operationDefinition = operationDefinitionMap[operationType]; + return ( + validation.input.includes(operationDefinition.input) && + (!validation.specificOperations || validation.specificOperations.includes(operationType)) && + validation.validateMetadata(column) + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 9f2b8eab4e09b..882252132c5b3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -293,6 +293,11 @@ describe('getOperationTypesForField', () => { "operationType": "median", "type": "field", }, + Object { + "field": "bytes", + "operationType": "percentile", + "type": "field", + }, Object { "field": "bytes", "operationType": "last_value", diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 58685fa494a04..c111983ea2cd6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -167,10 +167,13 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { operationDefinition.getPossibleOperation() ); } else if (operationDefinition.input === 'fullReference') { - addToMap( - { type: 'fullReference', operationType: operationDefinition.type }, - operationDefinition.getPossibleOperation() - ); + const validOperation = operationDefinition.getPossibleOperation(indexPattern); + if (validOperation) { + addToMap( + { type: 'fullReference', operationType: operationDefinition.type }, + validOperation + ); + } } }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts index 841011c588433..09132b142986f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimeScaleUnit } from '../time_scale'; -import { IndexPatternColumn } from './definitions'; +import type { IndexPatternLayer } from '../types'; +import type { TimeScaleUnit } from '../time_scale'; +import type { IndexPatternColumn } from './definitions'; import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange } from './time_scale_utils'; export const DEFAULT_TIME_SCALE = 's' as TimeScaleUnit; @@ -48,45 +49,71 @@ describe('time scale utils', () => { isBucketed: false, timeScale: 's', }; + const baseLayer: IndexPatternLayer = { + columns: { col1: baseColumn }, + columnOrder: [], + indexPatternId: '', + }; it('should keep column if there is no time scale', () => { const column = { ...baseColumn, timeScale: undefined }; - expect(adjustTimeScaleOnOtherColumnChange(column, { col1: column })).toBe(column); + expect( + adjustTimeScaleOnOtherColumnChange( + { ...baseLayer, columns: { col1: column } }, + 'col1', + 'col2' + ) + ).toBe(column); }); it('should keep time scale if there is a date histogram', () => { expect( - adjustTimeScaleOnOtherColumnChange(baseColumn, { - col1: baseColumn, - col2: { - operationType: 'date_histogram', - dataType: 'date', - isBucketed: true, - label: '', + adjustTimeScaleOnOtherColumnChange( + { + ...baseLayer, + columns: { + col1: baseColumn, + col2: { + operationType: 'date_histogram', + dataType: 'date', + isBucketed: true, + label: '', + sourceField: 'date', + params: { interval: 'auto' }, + }, + }, }, - }) + 'col1', + 'col2' + ) ).toBe(baseColumn); }); it('should remove time scale if there is no date histogram', () => { - expect(adjustTimeScaleOnOtherColumnChange(baseColumn, { col1: baseColumn })).toHaveProperty( + expect(adjustTimeScaleOnOtherColumnChange(baseLayer, 'col1', 'col2')).toHaveProperty( 'timeScale', undefined ); }); it('should remove suffix from label', () => { - expect(adjustTimeScaleOnOtherColumnChange(baseColumn, { col1: baseColumn })).toHaveProperty( - 'label', - 'Count of records' - ); + expect( + adjustTimeScaleOnOtherColumnChange( + { ...baseLayer, columns: { col1: baseColumn } }, + 'col1', + 'col2' + ) + ).toHaveProperty('label', 'Count of records'); }); it('should keep custom label', () => { const column = { ...baseColumn, label: 'abc', customLabel: true }; - expect(adjustTimeScaleOnOtherColumnChange(column, { col1: column })).toHaveProperty( - 'label', - 'abc' - ); + expect( + adjustTimeScaleOnOtherColumnChange( + { ...baseLayer, columns: { col1: column } }, + 'col1', + 'col2' + ) + ).toHaveProperty('label', 'abc'); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts index 5d525e573a617..340cad97e7db0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts @@ -5,8 +5,9 @@ */ import { unitSuffixesLong } from '../suffix_formatter'; -import { TimeScaleUnit } from '../time_scale'; -import { BaseIndexPatternColumn } from './definitions/column_types'; +import type { TimeScaleUnit } from '../time_scale'; +import type { IndexPatternLayer } from '../types'; +import type { IndexPatternColumn } from './definitions'; export const DEFAULT_TIME_SCALE = 's' as TimeScaleUnit; @@ -30,10 +31,13 @@ export function adjustTimeScaleLabelSuffix( return `${cleanedLabel} ${unitSuffixesLong[newTimeScale]}`; } -export function adjustTimeScaleOnOtherColumnChange( - column: T, - columns: Partial> -) { +export function adjustTimeScaleOnOtherColumnChange( + layer: IndexPatternLayer, + thisColumnId: string, + changedColumnId: string +): T { + const columns = layer.columns; + const column = columns[thisColumnId] as T; if (!column.timeScale) { return column; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index a5ce4dfbea371..38f51f24aae7d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -20,6 +20,7 @@ import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; import { OriginalColumn } from './rename_columns'; import { dateHistogramOperation } from './operations/definitions'; +import { getEsAggsSuffix } from './operations/definitions/helpers'; function getExpressionForLayer( layer: IndexPatternLayer, @@ -41,15 +42,20 @@ function getExpressionForLayer( expressions.push(...def.toExpression(layer, colId, indexPattern)); } else { aggs.push( - buildExpression({ type: 'expression', chain: [def.toEsAggsFn(col, colId, indexPattern)] }) + buildExpression({ + type: 'expression', + chain: [def.toEsAggsFn(col, colId, indexPattern, layer)], + }) ); } }); const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => { + const esAggsId = `col-${columnEntries.length === 1 ? 0 : index}-${colId}`; + const suffix = getEsAggsSuffix(column); return { ...currentIdMap, - [`col-${columnEntries.length === 1 ? 0 : index}-${colId}`]: { + [`${esAggsId}${suffix}`]: { ...column, id: colId, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 702930d02a90e..57cc4abeb723a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -5,7 +5,7 @@ */ import { DataType } from '../types'; -import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from './types'; +import { IndexPattern, IndexPatternLayer } from './types'; import { DraggedField } from './indexpattern'; import { BaseIndexPatternColumn, @@ -44,29 +44,6 @@ export function isDraggedField(fieldCandidate: unknown): fieldCandidate is Dragg ); } -export function hasInvalidColumns(state: IndexPatternPrivateState) { - return getInvalidLayers(state).length > 0; -} - -export function getInvalidLayers(state: IndexPatternPrivateState) { - return Object.values(state.layers).filter((layer) => { - return layer.columnOrder.some((columnId) => - isColumnInvalid(layer, columnId, state.indexPatterns[layer.indexPatternId]) - ); - }); -} - -export function getInvalidColumnsForLayer( - layers: IndexPatternLayer[], - indexPatternMap: Record -) { - return layers.map((layer) => { - return layer.columnOrder.filter((columnId) => - isColumnInvalid(layer, columnId, indexPatternMap[layer.indexPatternId]) - ); - }); -} - export function isColumnInvalid( layer: IndexPatternLayer, columnId: string, diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx index 5f18ef7c7f637..63261d08ff1a4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx @@ -140,6 +140,7 @@ export const getPieRenderer = (dependencies: { paletteService={dependencies.paletteService} onClickValue={onClickValue} renderMode={handlers.getRenderMode()} + syncColors={handlers.isSyncColorsEnabled()} /> , domNode, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 458b1a75c4c17..c6eed36f81ab0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -71,6 +71,7 @@ describe('PieVisualization component', () => { chartsThemeService, paletteService: chartPluginMock.createPaletteRegistry(), renderMode: 'display' as const, + syncColors: false, }; } @@ -172,6 +173,7 @@ describe('PieVisualization component', () => { { maxDepth: 2, totalSeries: 5, + syncColors: false, behindText: true, }, undefined diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 56ecf57f2dff7..b4c81cfb6e9c3 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -38,6 +38,15 @@ import { } from '../../../../../src/plugins/charts/public'; import { LensIconChartDonut } from '../assets/chart_donut'; +declare global { + interface Window { + /** + * Flag used to enable debugState on elastic charts + */ + _echDebugStateFlag?: boolean; + } +} + const EMPTY_SLICE = Symbol('empty_slice'); export function PieComponent( @@ -47,12 +56,13 @@ export function PieComponent( paletteService: PaletteRegistry; onClickValue: (data: LensFilterEvent['data']) => void; renderMode: RenderMode; + syncColors: boolean; } ) { const [firstTable] = Object.values(props.data.tables); const formatters: Record> = {}; - const { chartsThemeService, paletteService, onClickValue } = props; + const { chartsThemeService, paletteService, syncColors, onClickValue } = props; const { shape, groups, @@ -145,6 +155,7 @@ export function PieComponent( behindText: categoryDisplay !== 'hide', maxDepth: bucketColumns.length, totalSeries: totalSeriesCount, + syncColors, }, palette.params ); @@ -249,6 +260,7 @@ export function PieComponent( >