diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5a9e8bc585119..525da9d832b53 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -381,8 +381,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib **/*.scss @elastic/kibana-design #CC# /packages/kbn-ui-framework/ @elastic/kibana-design -# Core design +# Core UI design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers +/src/plugins/embeddable/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers diff --git a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md similarity index 91% rename from .github/ISSUE_TEMPLATE/security_solution_bug_report.md rename to .github/ISSUE_TEMPLATE/Bug_report_security_solution.md index bd7d57c72ea56..059e1d267c286 100644 --- a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md @@ -2,7 +2,7 @@ name: Bug report for Security Solution about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! title: '[Security Solution]' -labels: Team: SecuritySolution +labels: 'Team: SecuritySolution' --- **Describe the bug:** diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index 2099fb599ba67..d33b9603fae73 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -11,6 +11,8 @@ experimental[] Export dashboards and corresponding saved objects. `GET :/api/kibana/dashboards/export` +`GET :/s//api/kibana/dashboards/export` + [[dashboard-api-export-params]] ==== Query parameters diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 56bd4abbc8023..5d1fab41a2a14 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -11,6 +11,8 @@ experimental[] Import dashboards and corresponding saved objects. `POST :/api/kibana/dashboards/import` +`POST :/s//api/kibana/dashboards/import` + [[dashboard-api-import-params]] ==== Query parameters diff --git a/docs/api/features.asciidoc b/docs/api/features.asciidoc index 57a87ff6342f9..dad3ef75c8117 100644 --- a/docs/api/features.asciidoc +++ b/docs/api/features.asciidoc @@ -28,8 +28,6 @@ The API returns the following: { "id": "discover", "name": "Discover", - "icon": "discoverApp", - "navLinkId": "discover", "app": [ "kibana" ], @@ -73,8 +71,6 @@ The API returns the following: { "id": "visualize", "name": "Visualize", - "icon": "visualizeApp", - "navLinkId": "visualize", "app": [ "kibana" ], @@ -120,8 +116,6 @@ The API returns the following: { "id": "dashboard", "name": "Dashboard", - "icon": "dashboardApp", - "navLinkId": "dashboards", "app": [ "kibana" ], @@ -172,8 +166,6 @@ The API returns the following: { "id": "dev_tools", "name": "Dev Tools", - "icon": "devToolsApp", - "navLinkId": "dev_tools", "app": [ "kibana" ], diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index bc5e1ccc1dd55..7bdfe80b42177 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -18,12 +18,22 @@ image::apm/images/apm-alert.png[Create an alert in the APM app] For a walkthrough of the alert flyout panel, including detailed information on each configurable property, see Kibana's <>. -The APM app supports two different types of threshold alerts: transaction duration, and error rate. -Below, we'll create one of each. +The APM app supports four different types of alerts: + +* Transaction duration anomaly: +alerts when the service's transaction duration reaches a certain anomaly score +* Transaction duration threshold: +alerts when the service's transaction duration exceeds a given time limit over a given time frame +* Transaction error rate threshold: +alerts when the service's transaction error rate is above the selected rate over a given time frame +* Error count threshold: +alerts when service exceeds a selected number of errors over a given time frame + +Below, we'll walk through the creation of two of these alerts. [float] [[apm-create-transaction-alert]] -=== Create a transaction duration alert +=== Example: create a transaction duration alert Transaction duration alerts trigger when the duration of a specific transaction type in a service exceeds a defined threshold. This guide will create an alert for the `opbeans-java` service based on the following criteria: @@ -57,9 +67,9 @@ Enter a name for the connector, and paste the webhook URL. See Slack's webhook documentation if you need to create one. -Add a message body in markdown format. +A default message is provided as a starting point for your alert. You can use the https://mustache.github.io/[Mustache] template syntax, i.e., `{{variable}}` -to pass alert values at the time a condition is detected to an action. +to pass additional alert values at the time a condition is detected to an action. A list of available variables can be accessed by selecting the **add variable** button image:apm/images/add-variable.png[add variable button]. @@ -67,7 +77,7 @@ Select **Save**. The alert has been created and is now active! [float] [[apm-create-error-alert]] -=== Create an error rate alert +=== Example: create an error rate alert Error rate alerts trigger when the number of errors in a service exceeds a defined threshold. This guide creates an alert for the `opbeans-python` service based on the following criteria: @@ -94,9 +104,9 @@ Based on the alert criteria, define the following alert details: Select the **Email** action type and click **Create a connector**. Fill out the required details: sender, host, port, etc., and click **save**. -Add a message body in markdown format. +A default message is provided as a starting point for your alert. You can use the https://mustache.github.io/[Mustache] template syntax, i.e., `{{variable}}` -to pass alert values at the time a condition is detected to an action. +to pass additional alert values at the time a condition is detected to an action. A list of available variables can be accessed by selecting the **add variable** button image:apm/images/add-variable.png[add variable button]. diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index d53adb439f0c8..c405ea10ade3d 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -69,7 +69,7 @@ the host filter will still be applied. These filters are very useful for quickly and easily removing noise from your data. With just a click, you can filter your transactions by the transaction result, -host, container ID, and more. +host, container ID, Kubernetes pod, and more. [role="screenshot"] image::apm/images/local-filter.png[Local filters available in the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/images/advanced-discover.png b/docs/apm/images/advanced-discover.png index 56ba58b2c1d41..5291526783a6b 100644 Binary files a/docs/apm/images/advanced-discover.png and b/docs/apm/images/advanced-discover.png differ diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png index 350704d8969ae..c68b36f522bfc 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-distributed-tracing.png b/docs/apm/images/apm-distributed-tracing.png index e9c6713361c73..0dbffa591d43a 100644 Binary files a/docs/apm/images/apm-distributed-tracing.png and b/docs/apm/images/apm-distributed-tracing.png differ diff --git a/docs/apm/images/apm-error-group.png b/docs/apm/images/apm-error-group.png index ecdf9c20cf4aa..359bdc6b704e9 100644 Binary files a/docs/apm/images/apm-error-group.png and b/docs/apm/images/apm-error-group.png differ diff --git a/docs/apm/images/apm-errors-overview.png b/docs/apm/images/apm-errors-overview.png index 90f16b81e9f50..969a1f19f9f43 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 a767ed7e08e0c..3757127bad9c0 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-metrics.png b/docs/apm/images/apm-metrics.png index 60383ef428f2a..ffe5ffc7e1d83 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 313ee7d4b8fc8..90955fb61016d 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-service-map-anomaly.png b/docs/apm/images/apm-service-map-anomaly.png index b661e8f09d1a1..cd59f86690666 100644 Binary files a/docs/apm/images/apm-service-map-anomaly.png and b/docs/apm/images/apm-service-map-anomaly.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 48236522ddfbb..85d14cc7dfc6e 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 4eaef9ec15ac5..14cf32877b720 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-traces.png b/docs/apm/images/apm-traces.png index 6219be5b6d6e4..bf1f7e783bb11 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-response-dist.png b/docs/apm/images/apm-transaction-response-dist.png index ecf5a4af2c25d..1d268bbaac465 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 73668b094f9cf..bfdb6a5abe65b 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 b3b6ca22c4f63..53d7637b18647 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/example-metadata.png b/docs/apm/images/example-metadata.png index 0e35f90691723..2a5bda7f088f6 100644 Binary files a/docs/apm/images/example-metadata.png and b/docs/apm/images/example-metadata.png differ diff --git a/docs/apm/images/jvm-metrics-overview.png b/docs/apm/images/jvm-metrics-overview.png index 9c8ba4a12a262..586836c6cfe3e 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 1720e1370ff90..52a1ca5bea8d8 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 faac5c143a7d8..8657e39f430aa 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 e1a42f4c76e12..b3726bdc00ab6 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 078fabcfa2879..878a31adc69ca 100644 Binary files a/docs/apm/images/service-maps.png and b/docs/apm/images/service-maps.png differ diff --git a/docs/apm/images/service-quick-health.png b/docs/apm/images/service-quick-health.png new file mode 100644 index 0000000000000..aab1332513079 Binary files /dev/null and b/docs/apm/images/service-quick-health.png differ diff --git a/docs/apm/images/specific-transaction.png b/docs/apm/images/specific-transaction.png index 9911dbd879f41..52073bf76520a 100644 Binary files a/docs/apm/images/specific-transaction.png and b/docs/apm/images/specific-transaction.png differ diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc index db2a1ef6e2da0..b31d717a6932e 100644 --- a/docs/apm/machine-learning.asciidoc +++ b/docs/apm/machine-learning.asciidoc @@ -14,7 +14,12 @@ Machine learning jobs are created per environment, and are based on a service's Because jobs are created at the environment level, you can add new services to your existing environments without the need for additional machine learning jobs. -After a machine learning job is created, results are shown in two places: +Results from machine learning jobs are shown in multiple places throughout the APM app: + +* The **Services overview** provides a quick-glance view of the general health of all of your services. ++ +[role="screenshot"] +image::apm/images/service-quick-health.png[Example view of anomaly scores on response times in the APM app] * The transaction duration chart will show the expected bounds and add an annotation when the anomaly score is 75 or above. + diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index d629a95073a74..d44c4ff6caa5c 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -33,7 +33,7 @@ distributed tracing will not work, and the connection will not be drawn on the m Select the **Service Map** tab to get started. By default, all instrumented services and connections are shown. Whether you're onboarding a new engineer, or just trying to grasp the big picture, -click around, zoom in and out, and begin to visualize how your services are connected. +drag things around, zoom in and out, and begin to visualize how your services are connected. If there's a specific service that interests you, select that service to highlight its connections. Clicking **Focus map** will refocus the map on that specific service and lock the connection highlighting. diff --git a/docs/apm/services.asciidoc b/docs/apm/services.asciidoc index 395e23c379306..2bf2e35c21cd8 100644 --- a/docs/apm/services.asciidoc +++ b/docs/apm/services.asciidoc @@ -2,8 +2,13 @@ [[services]] === Services overview -The *Services* overview gives you quick insights into the health and general performance of all of your instrumented services. -Services are sorted by the `service.name` configured in each of the {apm-agents-ref}[APM agents] you’ve installed. +The *Services* overview page 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 <>. [role="screenshot"] -image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana] \ No newline at end of file +image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana] diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index c35fb115d2db4..7f29b1f003f1c 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -3,7 +3,7 @@ === Trace sample timeline The trace sample timeline visualization is a bird's-eye view of what your application was doing while it was trying to respond to a request. -This makes it useful for visualizing where the selected transaction spent most of its time. +This makes it useful for visualizing where a selected transaction spent most of its time. [role="screenshot"] image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM app in Kibana] @@ -43,9 +43,12 @@ this makes finding possible bottlenecks throughout your application much easier image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana] Don't forget; by definition, a distributed trace includes more than one transaction. -When viewing these distributed traces in the timeline waterfall, you'll see this image:apm/images/transaction-icon.png[APM icon] icon, +When viewing distributed traces in the timeline waterfall, +you'll see this icon: image:apm/images/transaction-icon.png[APM icon], which indicates the next transaction in the trace. -These transactions can be expanded and viewed in detail by clicking on them. +For easier problem isolation, transactions can be collapsed in the waterfall by clicking +the icon to the left of the transactions. +Transactions can also be expanded and viewed in detail by clicking on them. After exploring these traces, you can return to the full trace by clicking *View full trace*. diff --git a/docs/apm/traces.asciidoc b/docs/apm/traces.asciidoc index 52b4b618de466..3bafebd733159 100644 --- a/docs/apm/traces.asciidoc +++ b/docs/apm/traces.asciidoc @@ -7,7 +7,8 @@ and which services were part of it. In addition to the Traces overview, you can view your application traces in the <>. The *Traces* overview displays the entry transaction for all traces in your application. -If you're using <>, this view is key to finding the critical paths within your application. +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_. diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 84ab6b2a58579..fef98a86de1d0 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -10,17 +10,8 @@ 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 *time spent by span type*, *transaction duration*, and *requests per minute* chart display information on all transactions associated with the selected service: - -*Time spent 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? -+ -The time a transaction took to complete is also recorded and displayed on the chart under the "app" label. -"app" indicates that something was happening within the application, but we're not sure exactly what. -This could be a sign that the agent does not have auto-instrumentation for whatever was happening during that time. -+ -It's important to note that if you have asynchronous spans, the sum of all span times may exceed the duration of the transaction. +The *transaction duration*, *transactions per minute*, *transaction error rate*, and *time spent 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. @@ -28,11 +19,26 @@ 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. -*Requests per minute*:: +*Transactions per minute*:: 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*:: +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*:: +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? ++ +The time a transaction took to complete is also recorded and displayed on the chart under the "app" label. +"app" indicates that something was happening within the application, but we're not sure exactly what. +This could be a sign that the agent does not have auto-instrumentation for whatever was happening during that time. ++ +It's important to note that if you have asynchronous spans, the sum of all span times may exceed the duration of the transaction. + [[transactions-table]] ==== Transactions table @@ -61,42 +67,45 @@ refer to the documentation for each {apm-agents-ref}[APM Agent] you've implement ==== RUM Transaction overview The transaction overview page is customized for the JavaScript RUM Agent. -This page highlights things like *page load times*, *transactions per minute*, and even the *average page load duration distribution by country*. +Specifically, the page highlights *page load times* for your service: [role="screenshot"] image::apm/images/apm-geo-ui.png[average page load duration distribution] -This data is available due to the geo-ip and user agent pipelines being enabled by default, -which allows for the capture of geo-location and user agent data. -These visualizations make it easy for you to visualize performance information about your -end-users' experience based on their location. +Additional RUM goodies, like core vitals, and visitor breakdown by browser, location, and device, +are available in the Observability User Experience tab. +// To do +// Add link to the Observability UE docs when complete [[transaction-details]] ==== Transaction details Selecting a transaction group will bring you to the *transaction* details. -Transaction details include a high-level overview of the time spent by span type, -transaction group duration, requests per minute, and transaction group duration distribution. -It's important to note that all of these graphs show data from every transaction within the selected transaction group. +This page is visually similar to the transaction overview, but it shows data from all transactions within +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 your selection in the *Transactions duration distribution*. -You can update the sampled transactions by selecting a new _bucket_ in the transactions duration distribution graph. -The number of requests per bucket is displayed when hovering over the graph, and the selected bucket is highlighted to stand out. +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. + +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] -This graph shows a typical distribution, and indicates most of our requests were served quickly--awesome! -It's the requests on the right, the ones taking longer than average, that we probably want to focus on. - -When you select one of these buckets, +When you select a bucket, you're presented with up to ten trace samples. -Each sample has a trace timeline waterfall that shows what a typical request in that bucket was doing. -By investigating this timeline waterfall, we can hopefully determine _why_ this request was slow and then implement a fix. +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. +For large waterfalls, expand problematic transactions and collapse well-performing ones +for easier problem isolation and troubleshooting. [role="screenshot"] image::apm/images/apm-transaction-sample.png[Example view of transactions sample] diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index b4c9c6a4ec39e..7084777cbb6f9 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -14,6 +14,7 @@ Also, check out the https://discuss.elastic.co/c/apm[APM discussion forum]. * <> * <> * <> +* <> [float] [[no-apm-data-found]] @@ -156,7 +157,7 @@ the values in `http.request.cookies` are not indexed and thus not searchable. *Ensure an index pattern exists* As a first step, you should ensure the correct index pattern exists. -In Kibana, navigate to *Management > Kibana > Index Patterns*. +Open the main menu, then click *Stack Management > Index Patterns*. In the pattern list, you should see an apm index pattern; The default is `apm-*`. If you don't, the index pattern doesn't exist. See <> for information on how to fix this problem. @@ -180,3 +181,19 @@ setup.template.append_fields: type: object dynamic: true ---- + +[float] +[[service-map-rum-connections]] +=== Service maps: no connection between client and server + +If the service map is not showing an expected connection between the client and server, +it's likely because you haven't configured +{apm-agent-rum}/configuration.html#distributed-tracing-origins[`distributedTracingOrigins`]. + + +This setting is necessary, for example, for cross-origin requests. +If you have a basic web application that provides data via an API on `localhost:4000`, +and serves HTML from `localhost:4001`, you'd need to set `distributedTracingOrigins: ['https://localhost:4000']` +to ensure the origin is monitored as a part of distributed tracing. +In other words, `distributedTracingOrigins` is consulted prior to the agent adding the +distributed tracing `traceparent` header to each request. diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index 312391541a777..6456ba02bb8a8 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -14,7 +14,7 @@ For this tutorial, you'll need to add the <>, and work with data in other contexts. -To get started, open the menu, go to *Dev Tools*, then click *Painless Lab*. +To get started, open the main menu, click *Dev Tools*, then click *Painless Lab*. image::dev-tools/painlesslab/images/painless-lab.png[Painless Lab] diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc index eaa7fea6c7f8d..7cd54db5562b7 100644 --- a/docs/dev-tools/searchprofiler/getting-started.asciidoc +++ b/docs/dev-tools/searchprofiler/getting-started.asciidoc @@ -2,7 +2,7 @@ [[profiler-getting-started]] === Getting Started -The {searchprofiler} is automatically enabled in {kib}. From the menu, go to *Dev Tools*, then click *Search Profiler* +The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *Search Profiler* to get started. {searchprofiler} displays the names of the indices searched, the shards in each index, diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index b27e457940d93..8c80c2e5f2ffb 100644 --- a/docs/developer/architecture/security/feature-registration.asciidoc +++ b/docs/developer/architecture/security/feature-registration.asciidoc @@ -59,15 +59,6 @@ of features within the management screens. |See <> |The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher. -|`icon` -|`string` -|"discoverApp" -|An https://elastic.github.io/eui/#/display/icons[EUI Icon] to use for this feature. - -|`navLinkId` -|`string` -|"sample_app" -|The ID of the navigation link associated with your feature. |=== ==== Privilege definition @@ -100,8 +91,6 @@ public setup(core, { features }) { features.registerKibanaFeature({ id: 'canvas', name: 'Canvas', - icon: 'canvasApp', - navLinkId: 'canvas', category: DEFAULT_APP_CATEGORIES.kibana, app: ['canvas', 'kibana'], catalogue: ['canvas'], @@ -160,8 +149,6 @@ public setup(core, { features }) { name: i18n.translate('xpack.features.devToolsFeatureName', { defaultMessage: 'Dev Tools', }), - icon: 'devToolsApp', - navLinkId: 'dev_tools', category: DEFAULT_APP_CATEGORIES.management, app: ['kibana'], catalogue: ['console', 'searchprofiler', 'grokdebugger'], @@ -223,8 +210,6 @@ public setup(core, { features }) { defaultMessage: 'Discover', }), order: 100, - icon: 'discoverApp', - navLinkId: 'discover', category: DEFAULT_APP_CATEGORIES.kibana, app: ['kibana'], catalogue: ['discover'], diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md index 0a49ee6e63d6c..1134994faa9bd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md @@ -30,6 +30,7 @@ export declare class KibanaRequestboolean | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the HttpFetchOptions#asSystemRequest option. | | [params](./kibana-plugin-core-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-core-server.kibanarequest.query.md) | | Query | | +| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | Url | URL rewritten in onPreRouting request interceptor. | | [route](./kibana-plugin-core-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | | [socket](./kibana-plugin-core-server.kibanarequest.socket.md) | | IKibanaSocket | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | | [url](./kibana-plugin-core-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md new file mode 100644 index 0000000000000..10628bafaf1d4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) > [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) + +## KibanaRequest.rewrittenUrl property + +URL rewritten in onPreRouting request interceptor. + +Signature: + +```typescript +readonly rewrittenUrl?: Url; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md index c5e01715534d1..ad762cae489c8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md @@ -43,4 +43,5 @@ export declare enum ES_FIELD_TYPES | STRING | "string" | | | TEXT | "text" | | | TOKEN\_COUNT | "token_count" | | +| UNSIGNED\_LONG | "unsigned_long" | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md new file mode 100644 index 0000000000000..26276a809a613 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [deleteFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md) + +## IndexPattern.deleteFieldFormat property + +Signature: + +```typescript +deleteFieldFormat: (fieldName: string) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md new file mode 100644 index 0000000000000..0dd171108b20b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getFormatterForFieldNoDefault](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md) + +## IndexPattern.getFormatterForFieldNoDefault() method + +Get formatter for a given field name. Return undefined if none exists + +Signature: + +```typescript +getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldname | string | | + +Returns: + +`FieldFormat | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index c07041470d102..7e3192481dfff 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -20,6 +20,7 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [deleteFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | @@ -30,6 +31,7 @@ export declare class IndexPattern implements IIndexPattern | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | | [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md) | | () => void | Reset last saved saved object fields. used after saving | +| [setFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md) | | (fieldName: string, format: SerializedFieldFormat) => void | | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | @@ -47,6 +49,7 @@ export declare class IndexPattern implements IIndexPattern | [getComputedFields()](./kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md) | | | | [getFieldByName(name)](./kibana-plugin-plugins-data-public.indexpattern.getfieldbyname.md) | | | | [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | +| [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists | | [getNonScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md) | | | | [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md new file mode 100644 index 0000000000000..9774fc8c7308c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [setFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md) + +## IndexPattern.setFieldFormat property + +Signature: + +```typescript +setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md new file mode 100644 index 0000000000000..af4115e4c4e09 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [fieldFormats](./kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md) + +## IndexPatternSpec.fieldFormats property + +Signature: + +```typescript +fieldFormats?: Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md index 74c4df126e1bf..f3b692209ca67 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -14,6 +14,7 @@ export interface IndexPatternSpec | Property | Type | Description | | --- | --- | --- | +| [fieldFormats](./kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md) | Record<string, SerializedFieldFormat> | | | [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | | [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) | string | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md index d071955f4f522..545b7b9d27e10 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md @@ -43,4 +43,5 @@ export declare enum ES_FIELD_TYPES | STRING | "string" | | | TEXT | "text" | | | TOKEN\_COUNT | "token_count" | | +| UNSIGNED\_LONG | "unsigned_long" | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md new file mode 100644 index 0000000000000..4bfda56527474 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [deleteFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md) + +## IndexPattern.deleteFieldFormat property + +Signature: + +```typescript +deleteFieldFormat: (fieldName: string) => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md new file mode 100644 index 0000000000000..77cc879e2f2f2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFormatterForFieldNoDefault](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md) + +## IndexPattern.getFormatterForFieldNoDefault() method + +Get formatter for a given field name. Return undefined if none exists + +Signature: + +```typescript +getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldname | string | | + +Returns: + +`FieldFormat | undefined` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 603864234d34b..2e15c8d3867ec 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -20,6 +20,7 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [deleteFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | @@ -30,6 +31,7 @@ export declare class IndexPattern implements IIndexPattern | [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) | | string[] | | | [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md) | | () => void | Reset last saved saved object fields. used after saving | +| [setFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md) | | (fieldName: string, format: SerializedFieldFormat) => void | | | [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) | | string | | @@ -47,6 +49,7 @@ export declare class IndexPattern implements IIndexPattern | [getComputedFields()](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) | | | | [getFieldByName(name)](./kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md) | | | | [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | +| [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists | | [getNonScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) | | | | [getScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md new file mode 100644 index 0000000000000..a8f2e726dd9b3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [setFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md) + +## IndexPattern.setFieldFormat property + +Signature: + +```typescript +setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md index d36ebd0745e8d..214c795fda9d1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md @@ -9,12 +9,13 @@ Constructs a new instance of the `IndexPatternsFetcher` class Signature: ```typescript -constructor(callDataCluster: LegacyAPICaller); +constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| callDataCluster | LegacyAPICaller | | +| elasticsearchClient | ElasticsearchClient | | +| allowNoIndices | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md index 52382372d6d96..addd29916d81d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md @@ -13,7 +13,7 @@ getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; fieldCapsOptions?: { - allowNoIndices: boolean; + allow_no_indices: boolean; }; }): Promise; ``` @@ -22,7 +22,7 @@ getFieldsForWildcard(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allowNoIndices: boolean;
};
} | | +| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
} | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md index f71a702f3381d..3ba3c862bf16a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md @@ -14,7 +14,7 @@ export declare class IndexPatternsFetcher | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(callDataCluster)](./kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md) | | Constructs a new instance of the IndexPatternsFetcher class | +| [(constructor)(elasticsearchClient, allowNoIndices)](./kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md) | | Constructs a new instance of the IndexPatternsFetcher class | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md new file mode 100644 index 0000000000000..ad1de0cc5f45b --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) > [cumulative\_sum](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md) + +## ExpressionFunctionDefinitions.cumulative\_sum property + +Signature: + +```typescript +cumulative_sum: ExpressionFunctionCumulativeSum; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md index 914c5d6ebe2f6..d1703a1e019e6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md @@ -17,6 +17,7 @@ export interface ExpressionFunctionDefinitions | Property | Type | Description | | --- | --- | --- | | [clog](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.clog.md) | ExpressionFunctionClog | | +| [cumulative\_sum](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum | | | [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | | [kibana\_context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext | | | [kibana](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md new file mode 100644 index 0000000000000..2fb8cde92e877 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) > [cumulative\_sum](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md) + +## ExpressionFunctionDefinitions.cumulative\_sum property + +Signature: + +```typescript +cumulative_sum: ExpressionFunctionCumulativeSum; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md index 71cd0b98a68c2..05b4ddce4ccde 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md @@ -17,6 +17,7 @@ export interface ExpressionFunctionDefinitions | Property | Type | Description | | --- | --- | --- | | [clog](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.clog.md) | ExpressionFunctionClog | | +| [cumulative\_sum](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum | | | [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | | [kibana\_context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext | | | [kibana](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana | | diff --git a/docs/discover/images/Discover-Start.png b/docs/discover/images/Discover-Start.png index fb885c20c1cf7..12ec2f9889bbd 100644 Binary files a/docs/discover/images/Discover-Start.png and b/docs/discover/images/Discover-Start.png differ diff --git a/docs/discover/images/time-filter.png b/docs/discover/images/time-filter.png new file mode 100644 index 0000000000000..f6d1d5809d7eb Binary files /dev/null and b/docs/discover/images/time-filter.png differ diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index f306f2b8f763f..35f1160ee834d 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -115,7 +115,7 @@ KQL supports `>`, `>=`, `<`, and `<=`. For example: [source,yaml] ------------------- -account_number:>=100 and items_sold:<=200 +account_number >= 100 and items_sold <= 200 ------------------- [discrete] diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index ee1e1526f9d6f..3720a5b457d84 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -104,9 +104,7 @@ 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 menu, then click *Stack Management. -. From the {kib} menu, click *Saved Ojbects*. +To import, export, and delete saved searches, open the main menu, then click *Stack Management > Saved Ojbects*. ==== Open a saved search To load a saved search into Discover: diff --git a/docs/discover/set-time-filter.asciidoc b/docs/discover/set-time-filter.asciidoc index 93fdf9ffd695a..dcdc8ee791e83 100644 --- a/docs/discover/set-time-filter.asciidoc +++ b/docs/discover/set-time-filter.asciidoc @@ -30,7 +30,7 @@ to the last 15 minutes. * *Refresh every* to specify an automatic refresh rate. + [role="screenshot"] -image::images/Timepicker-View.png[Time filter menu] +image::images/time-filter.png[Time filter menu] . To set the start and end times, click the bar next to the time filter. In the popup, select *Absolute*, *Relative* or *Now*, then specify the required diff --git a/docs/fleet/images/fleet-start.png b/docs/fleet/images/fleet-start.png index 60e5416fde127..0d0f7b8feec9c 100644 Binary files a/docs/fleet/images/fleet-start.png and b/docs/fleet/images/fleet-start.png differ diff --git a/docs/getting-started/images/add-sample-data.png b/docs/getting-started/images/add-sample-data.png index b8c2002b9c4cd..9dee27dcde71b 100644 Binary files a/docs/getting-started/images/add-sample-data.png and b/docs/getting-started/images/add-sample-data.png differ diff --git a/docs/getting-started/images/tutorial-sample-dashboard.png b/docs/getting-started/images/tutorial-sample-dashboard.png index 9f287640f201c..4c95c04c5e43e 100644 Binary files a/docs/getting-started/images/tutorial-sample-dashboard.png and b/docs/getting-started/images/tutorial-sample-dashboard.png differ diff --git a/docs/getting-started/images/tutorial-sample-filter.png b/docs/getting-started/images/tutorial-sample-filter.png index 7c1d041448557..56ebacadbef45 100644 Binary files a/docs/getting-started/images/tutorial-sample-filter.png and b/docs/getting-started/images/tutorial-sample-filter.png differ diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index 6386feac5ab49..f239b7ae6ca88 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -10,8 +10,9 @@ When you've finished, you'll know how to: * <> [float] -=== Before you begin -When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. For more information, refer to {ref}/security-privileges.html[Security privileges]. +=== Required privileges +When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. +For more information, refer to {ref}/security-privileges.html[Security privileges]. [float] [[set-up-on-cloud]] @@ -30,7 +31,7 @@ Sample data sets come with sample visualizations, dashboards, and more to help y . On the *Sample eCommerce orders* card, click *Add data*. + [role="screenshot"] -image::getting-started/images/add-sample-data.png[] +image::getting-started/images/add-sample-data.png[Add data UI] [float] [[explore-the-data]] @@ -38,7 +39,7 @@ image::getting-started/images/add-sample-data.png[] *Discover* displays an interactive histogram that shows the distribution of of data, or documents, over time, and a table that lists the fields for each document that matches the index. By default, all fields are shown for each matching document. -. Open the menu, then click *Discover*. +. Open the main menu, then click *Discover*. . Change the <> to *Last 7 days*. + @@ -70,7 +71,7 @@ For more information, refer to <>. A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, Markdown, and more. -. Open the menu, then click *Dashboard*. +. Open the main menu, then click *Dashboard*. . Click *[eCommerce] Revenue Dashboard*. + @@ -83,7 +84,7 @@ image::getting-started/images/tutorial-sample-dashboard.png[] To focus in on the data you want to view on the dashboard, use filters. -. From the *Controls* visualization, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*. +. From the *[eCommerce] Controls* panel, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*. + For example, the following dashboard shows the data for women's clothing from Gnomehouse. + @@ -103,11 +104,11 @@ For more information, refer to <>. [float] [[create-a-visualization]] -=== Create a visualization +=== Create a visualization panel -To create a treemap that shows the top regions and manufacturers, use *Lens*, then add the treemap to the dashboard. +To create a treemap panel that shows the top regions and manufacturers, use *Lens*, then add the treemap panel to the dashboard. -. From the {kib} toolbar, click *Edit*, then click *Create new*. +. From the toolbar, click *Edit*, then click *Create new*. . On the *New Visualization* window, click *Lens*. @@ -126,7 +127,7 @@ image::getting-started/images/tutorial-visualization-dropdown.png[Visualization . On the *Save Lens visualization*, enter a title and make sure *Add to Dashboard after saving* is selected, then click *Save and return*. + -The treemap appears as the last visualization on the dashboard. +The treemap appears as the last visualization panel on the dashboard. + [role="screenshot"] image::getting-started/images/tutorial-final-dashboard.gif[Final dashboard with new treemap visualization] diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 8e8d0e5bf996e..293597685ecc0 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -6,7 +6,7 @@ behavior of Kibana. For example, you can change the format used to display dates specify the default index pattern, and set the precision for displayed decimal values. -. Open the menu, then go to *Stack Management > {kib} > Advanced Settings*. +. Open the main menu, then click *Stack Management > Advanced Settings*. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. . Click *Save changes*. diff --git a/docs/management/alerting/alerts-and-actions-intro.asciidoc b/docs/management/alerting/alerts-and-actions-intro.asciidoc index 429d7915cc1c3..0c7ca7f1db17d 100644 --- a/docs/management/alerting/alerts-and-actions-intro.asciidoc +++ b/docs/management/alerting/alerts-and-actions-intro.asciidoc @@ -6,8 +6,8 @@ beta[] The *Alerts and Actions* UI lets you <> in a space, and provides tools to <> so that alerts can trigger actions like notification, indexing, and ticketing. -To manage alerting and connectors, open the menu, -then go to *Stack Management > Alerts and Insights > Alerts and Actions*. +To manage alerting and connectors, open the main menu, +then click *Stack Management > Alerts and Insights > Alerts and Actions*. [role="screenshot"] image:management/alerting/images/alerts-and-actions-ui.png[Example alert listing in the Alerts and Actions UI] diff --git a/docs/management/images/management-index-templates-mappings.png b/docs/management/images/management-index-templates-mappings.png old mode 100755 new mode 100644 index 62321fc0e4666..beb964b348171 Binary files a/docs/management/images/management-index-templates-mappings.png and b/docs/management/images/management-index-templates-mappings.png differ diff --git a/docs/management/images/management-index-templates.png b/docs/management/images/management-index-templates.png old mode 100755 new mode 100644 index 6f2564af72b5c..07f1fb9a7add1 Binary files a/docs/management/images/management-index-templates.png and b/docs/management/images/management-index-templates.png differ diff --git a/docs/management/images/management_index_component_template.png b/docs/management/images/management_index_component_template.png new file mode 100644 index 0000000000000..c03029fd172f0 Binary files /dev/null and b/docs/management/images/management_index_component_template.png differ diff --git a/docs/management/images/management_index_create_wizard.png b/docs/management/images/management_index_create_wizard.png old mode 100755 new mode 100644 index b18c36366be94..bff1dd4cd0e7a Binary files a/docs/management/images/management_index_create_wizard.png and b/docs/management/images/management_index_create_wizard.png differ diff --git a/docs/management/images/management_index_data_stream_backing_index.png b/docs/management/images/management_index_data_stream_backing_index.png new file mode 100644 index 0000000000000..a5c577affbbb2 Binary files /dev/null and b/docs/management/images/management_index_data_stream_backing_index.png differ diff --git a/docs/management/images/management_index_data_stream_stats.png b/docs/management/images/management_index_data_stream_stats.png new file mode 100644 index 0000000000000..a67ab4a7deb32 Binary files /dev/null and b/docs/management/images/management_index_data_stream_stats.png differ diff --git a/docs/management/images/management_index_details.png b/docs/management/images/management_index_details.png index 77aeaba472307..b199d13218f5a 100644 Binary files a/docs/management/images/management_index_details.png and b/docs/management/images/management_index_details.png differ diff --git a/docs/management/images/management_index_labels.png b/docs/management/images/management_index_labels.png old mode 100755 new mode 100644 index 79e378e367e78..a89c32e08beff Binary files a/docs/management/images/management_index_labels.png and b/docs/management/images/management_index_labels.png differ diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 7de2a042160e9..e83e6d262f26c 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -25,8 +25,8 @@ image::images/management-index-read-only-badge.png[Example of Index Pattern Mana [[settings-create-pattern]] === Create an index pattern -When you don't have an index pattern, {kib} prompts you to create one. Or, you can open the menu, -then go to *Stack Management > {kib} > Index Patterns* to go directly to the *Index Patterns* UI. +When you don't have an index pattern, {kib} prompts you to create one. Or, you can open the main menu, +then click *Stack Management > Index Patterns*. [role="screenshot"] image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollup index pattern"] diff --git a/docs/management/index-patterns/images/index-pattern-ui.png b/docs/management/index-patterns/images/index-pattern-ui.png new file mode 100644 index 0000000000000..7d16540aa03a2 Binary files /dev/null and b/docs/management/index-patterns/images/index-pattern-ui.png differ diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc index 7986e4e56279a..d9745bfef524a 100644 --- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc +++ b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc @@ -7,7 +7,7 @@ pipelines that perform common transformations and enrichments on your data. For example, you might remove a field, rename an existing field, or set a new field. -You’ll find *Ingest Node Pipelines* in *Stack Management > Ingest*. With this feature, you can: +To begin, open the main menu, then click *Stack Management > Ingest Node Pipelines*. With *Ingest Node Pipelines*, you can: * View a list of your pipelines and drill down into details. * Create a pipeline that defines a series of tasks, known as processors. @@ -23,7 +23,7 @@ image:management/ingest-pipelines/images/ingest-pipeline-list.png["Ingest node p The minimum required permissions to access *Ingest Node Pipelines* are the `manage_pipeline` and `cluster:monitor/nodes/info` cluster privileges. -You can add these privileges in *Stack Management > Security > Roles*. +To add privileges, open the main menu, then click *Stack Management > Roles*. [role="screenshot"] image:management/ingest-pipelines/images/ingest-pipeline-privileges.png["Privileges required for Ingest Node Pipelines"] diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc index 678e160b99af0..10c98cca26345 100644 --- a/docs/management/managing-beats.asciidoc +++ b/docs/management/managing-beats.asciidoc @@ -4,7 +4,7 @@ include::{asciidoc-dir}/../../shared/discontinued.asciidoc[tag=cm-discontinued] -To use {beats} Central Management UI, open the menu, go to *Stack Management > Ingest > +To use {beats} Central Management, open the main menu, click *Stack Management > {beats} Central Management*, then define and manage configurations in a central location in {kib} and quickly deploy configuration changes to all {beats} running across your enterprise. For more @@ -18,8 +18,8 @@ about central management, see the related {beats} documentation: This feature requires an Elastic license that includes {beats} central management. -Don't have a license? You can start a 30-day trial. Open the menu, -go to *Stack Management > Stack > License Management*. At the end of the trial +Don't have a license? You can start a 30-day trial. Open the main menu, then +click *Stack Management > License Management*. At the end of the trial period, you can purchase a subscription to keep using central management. For more information, see https://www.elastic.co/subscriptions and <>. diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index ad3a0ef0fcdd1..441bce43c7cdf 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -134,7 +134,7 @@ https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless [[create-scripted-field]] === Create a scripted field -. Open the menu, then go to *Stack Management > {kib} > Index Patterns* +. Open the main menu, then click *Stack Management > Index Patterns*. . Select the index pattern you want to add a scripted field to. . Go to the *Scripted fields* tab for the index pattern, then click *Add scripted field*. . Enter a name for the scripted field. diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc index 24cd094c877c6..8416c164c6c51 100644 --- a/docs/management/managing-indices.asciidoc +++ b/docs/management/managing-indices.asciidoc @@ -2,32 +2,40 @@ [[managing-indices]] == Index Management -*Index Management* enables you to view index settings, -mappings, and statistics and perform index-level operations. -These include refreshing, flushing, clearing the cache, force merging segments, -freezing indices, and more. Practicing good index management helps ensure -that your data is stored in the most cost-effective way possible. +*Index Management* features are an easy, convenient way to manage your +{es} cluster's indices, data streams, and index templates. Practicing good index +management ensures your data is stored correctly and in the most cost-effective +way possible. -*Index Management* also helps you create index templates. A template reduces -the amount of bookkeeping when working with indices. Instead of manually -setting up your indices, you can create them automatically from a template, -ensuring that your settings, mappings, and aliases are consistently defined. +[float] +=== What you'll learn + +This page shows you how to use *Index Management* features to: -To manage your indices, open the menu, then go to *Stack Management > Data > Index Management*. +To manage your indices, open the main menu, then click *Stack Management > Index Management*. [role="screenshot"] image::images/management_index_labels.png[Index Management UI] -If security is enabled, -you must have the `monitor` cluster privilege and the `view_index_metadata` +[float] +=== Before you start + +Before using this feature, you should be familiar with index management +operations. Refer to the {ref}/indices.html[index management APIs], the +{ref}/indices-templates.html[index template APIs], and the +{ref}/data-streams.html[data streams documentation]. + +[float] +=== Required permissions + +The minimum required permissions to access *Index Management* are +the `monitor` cluster privilege and the `view_index_metadata` and `manage` index privileges to view the data. For index templates, you must have the `manage_index_templates` cluster privilege. See {ref}/security-privileges.html[Security privileges] for more information. -Before using this feature, you should be familiar with index management -operations. Refer to the {ref}/indices.html[index management APIs] -and the {ref}/indices-templates.html[index template APIs]. +You can add these privileges in *Stack Management > Security > Roles*. [float] === View and edit indices @@ -50,7 +58,7 @@ image::images/management_index_details.png[Index Management UI] [float] === Perform index-level operations -Use the *Manage* menu to perform index-level operations. This menu +Use the *Manage* menu to perform index-level operations. This menu is available in the index details view, or when you select the checkbox of one or more indices on the overview page. The menu includes the following actions: @@ -78,16 +86,37 @@ searchable, but queries take longer. * *Delete index*. Permanently removes the index and all of its documents. -* *Add lifecycle policy*. Specifies a policy for managing the lifecycle of the +* *Add lifecycle policy*. Specifies a policy for managing the lifecycle of the index. +[float] +[[manage-data-streams]] +=== Manage data streams + +A {ref}/data-streams.html[data stream] lets you store time series data across +multiple backing indices while giving you a single named resource to use in +requests. The *Data Streams* view lists your data streams and lets you examine +or delete them. + +To view more information about a data stream, such as its generation or its +current index lifecycle policy, click the stream's name. + +[role="screenshot"] +image::images/management_index_data_stream_stats.png[Data stream details] + +To view information about the stream's backing indices, click the number in the +*Indices* column. + +[role="screenshot"] +image::images/management_index_data_stream_backing_index.png[Backing index] + [float] [[manage-index-templates]] === Manage index templates An index template defines {ref}/index-modules.html#index-modules-settings[settings], {ref}/mapping.html[mappings], and {ref}/indices-add-alias.html[aliases] -that you can automatically apply when creating a new index. {es} applies a +that you can automatically apply when creating a new index. {es} applies a template to a new index based on an index pattern that matches the index name. The *Index Templates* view lists your templates and enables you to examine, edit, clone, and @@ -103,33 +132,56 @@ so you must create the template before you create the indices. [float] -==== Example: Create an index template +==== Try it: Create an index template -In this example, you’ll create an index template for randomly generated log files. +In this tutorial, you’ll create an index template for randomly generated log +files. You'll then use the template to configure two new indices. -Open the *Create template* wizard, and enter `logs_template` in the *Name* -field. Set *Index pattern* to `logstash*` so the template matches any index -with that index pattern. The merge order and version are both optional, -and you'll leave them blank in this example. +*Step 1. Add a name and index pattern* +. In the *Index Templates* view, open the *Create template* wizard. ++ [role="screenshot"] image::images/management_index_create_wizard.png[Create wizard] -The second step in the *Create template* wizard allows you to define index settings. -These settings are optional, and this example skips this step. +. In the *Name* field, enter `my-index-template`. + +. Set *Index pattern* to `my-index-*` so the template matches any index +with that index pattern. -The logs data set requires a -mapping to label the latitude and longitude pairs as geographic locations -by applying the geo_point type. In the third step of the wizard, define this mapping -under the *Mapped fields* tab as follows: +. Leave *Data Stream*, *Priority*, *Version*, and *_meta field* as-is or blank. +. Click *Next*. + +*Step 2. Add settings, mappings, and index aliases* + +. Add component templates to your index template. ++ +{ref}/indices-component-template.html[Component templates] are pre-configured +sets of mappings, index settings, and index aliases you can reuse across +multiple index templates. Badges indicate whether a component template contains +mappings (*M*), index settings (*S*), index aliases (*A*), or a combination of +the three. ++ +Component templates are optional. For this tutorial, do not add any component +templates. ++ [role="screenshot"] -image::images/management-index-templates-mappings.png[Mapped fields page] +image::images/management_index_component_template.png[Component templates page] -Alternatively, you can click the *Load JSON* link and define the mapping as JSON: +. Define index settings. These are optional. For this tutorial, leave this +section blank. +. Define a mapping that contains an object field named `geo` with a child +geo-point field named `coordinates`: ++ +[role="screenshot"] +image::images/management-index-templates-mappings.png[Mapped fields page] ++ +Alternatively, you can click the *Load JSON* link and define the mapping as JSON: ++ [source,js] ----------------------------------- +---- { "properties": { "geo": { @@ -141,28 +193,33 @@ Alternatively, you can click the *Load JSON* link and define the mapping as JSON } } } ----------------------------------- - +---- ++ You can create additional mapping configurations in the *Dynamic templates* and -*Advanced options* tabs. No additional mappings are required for this example. - -In the fourth step, define an alias named `logstash`. +*Advanced options* tabs. No additional mappings are required for this tutorial. +. Define an index alias named `my-index`: ++ [source,js] ----------------------------------- +---- { - "logstash": {} + "my-index": {} } ----------------------------------- +---- + +. On the review page, check the summary. If everything looks right, click +*Create template*. -A summary of the template is in step 5. If everything looks right, click *Create template*. +*Step 3. Create new indices* -At this point, you’re ready to use the {es} index API to load the logs data. -In the {kib} *Console*, index two documents: +You’re now ready to load the logs data and create new indices using your index +template. +. In the {kib} *Console*, index the following documents: ++ [source,js] ----------------------------------- -POST /logstash-2019.05.18/_doc +---- +POST /my-index-000001/_doc { "@timestamp": "2019-05-18T15:57:27.541Z", "ip": "225.44.217.191", @@ -177,7 +234,7 @@ POST /logstash-2019.05.18/_doc "url": "https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/charles-fullerton.jpg" } -POST /logstash-2019.05.20/_doc +POST /my-index-000002/_doc { "@timestamp": "2019-05-20T03:44:20.844Z", "ip": "198.247.165.49", @@ -192,7 +249,10 @@ POST /logstash-2019.05.20/_doc "memory": 241720, "url": "https://theacademyofperformingartsandscience.org/people/type:astronauts/name:laurel-b-clark/profile" } ----------------------------------- +---- ++ +These requests create two indices: `my-index-000001` and `my-index-000002`. -The mappings and alias are configured automatically based on the template. To verify, you -can view one of the newly created indices using the {ref}/indices-get-index.html#indices-get-index[index API]. +. Use the {es} {ref}/indices-get-index.html#indices-get-index[get index API] to +view one of the newly created indices. The index's mappings and alias are +configured automatically based on the template. diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index b53bda95466dc..8944414f6bfbc 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -7,7 +7,7 @@ with no expiration date. For the full list of features, refer to If you want to try out the full set of features, you can activate a free 30-day trial. To view the status of your license, start a trial, or install a new -license, open the menu, then go to *Stack Management > Stack > License Management*. +license, open the main menu, then click *Stack Management > License Management*. NOTE: You can start a trial only if your cluster has not already activated a trial license for the current major product version. For example, if you have @@ -34,7 +34,7 @@ the features that will no longer be supported if you revert to a basic license. The `manage` cluster privilege is required to access *License Management*. -You can add this privilege in *Stack Management > Security > Roles*. +To add the privilege, open the main menu, then click *Stack Management > Roles*. [discrete] [[update-license]] diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 8c885ddca52e5..639be87c540fb 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -5,13 +5,7 @@ The *Saved Objects* UI helps you keep track of and manage your saved objects. Th store data for later use, including dashboards, visualizations, maps, index patterns, Canvas workpads, and more. -To get started, open the menu, then go to *Stack Management > {kib} > Saved Objects*. With this UI, you can: - -* <> -* <> -* <> -* <> - +To get started, open the main menu, then click *Stack Management > Saved Objects*. [role="screenshot"] image::images/management-saved-objects.png[Saved Objects] diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 7324f45594bd7..bc876ab67bc62 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -8,11 +8,7 @@ by an index pattern, and then rolls it into a new index. Rollup indices are a go compactly store months or years of historical data for use in visualizations and reports. -To get started, open the menu, then go to *Stack Management > Data > Rollup Jobs*. With this UI, -you can: - -* <> -* <> +To get started, open the main menu, then click *Stack Management > Rollup Jobs*. [role="screenshot"] image::images/management_rollup_list.png[][List of currently active rollup jobs] @@ -25,7 +21,7 @@ Before using this feature, you should be familiar with how rollups work. The `manage_rollup` cluster privilege is required to access *Rollup jobs*. -You can add this privilege in *Stack Management > Security > Roles*. +To add the privilege, open the main menu, then click *Stack Management > Roles*. [float] [[create-and-manage-rollup-job]] @@ -137,7 +133,7 @@ Your next step is to visualize your rolled up data in a vertical bar chart. Most visualizations support rolled up data, with the exception of Timelion and Vega visualizations. -. Go to *Stack Management > {kib} > Index Patterns*. +. Open the main menu, then click *Stack Management > Index Patterns*. . Click *Create index pattern*, and select *Rollup index pattern* from the dropdown. + @@ -152,7 +148,7 @@ is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_log matches the rolled up index pattern and `kibana_sample_data_logs` matches the index pattern for raw data. -. Go to *Dashboard* and create a vertical bar chart. +. Open the main menu, click *Dashboard*, then create and add a vertical bar chart. . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc index 1bf62522e245c..62633441ef161 100644 --- a/docs/management/snapshot-restore/index.asciidoc +++ b/docs/management/snapshot-restore/index.asciidoc @@ -8,7 +8,7 @@ Snapshots are important because they provide a copy of your data in case something goes wrong. If you need to roll back to an older version of your data, you can restore a snapshot from the repository. -To get started, open the menu, then go to *Stack Management > Data > Snapshot and Restore*. +To get started, open the main menu, then click *Stack Management > Snapshot and Restore*. With this UI, you can: * Register a repository for storing your snapshots @@ -32,7 +32,7 @@ The minimum required permissions to access *Snapshot and Restore* include: * Cluster privileges: `monitor`, `manage_slm`, `cluster:admin/snapshot`, and `cluster:admin/repository` * Index privileges: `all` on the `monitor` index if you want to access content in the *Restore Status* tab -To add privileges, open the menu, then go to *Stack Management > Security > Roles*. +To add privileges, open the main menu, then click *Stack Management > Roles*. [role="screenshot"] image:management/snapshot-restore/images/snapshot_permissions.png["Edit Role"] @@ -191,7 +191,7 @@ your master and data nodes. You can do this in one of two ways: Use *Snapshot and Restore* to register the repository where your snapshots will live. -. Open the menu, then go to *Stack Management > Data > Snapshot and Restore*. +. Open the main menu, then click *Stack Management > Snapshot and Restore*. . Click *Register a repository* in either the introductory message or *Repository view*. . Enter a name for your repository, for example, `my_backup`. . Select *Shared file system*. @@ -212,7 +212,7 @@ The repository currently doesn’t have any snapshots. ==== Add a snapshot to the repository Use the {ref}/snapshots-take-snapshot.html[snapshot API] to create a snapshot. -. Open the menu, go to *Dev Tools*, then select *Console*. +. Open the main menu, click *Dev Tools*, then select *Console*. . Create the snapshot: + [source,js] diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc index 2b8c2da2ef577..61df6457a9bde 100644 --- a/docs/management/upgrade-assistant/index.asciidoc +++ b/docs/management/upgrade-assistant/index.asciidoc @@ -4,7 +4,7 @@ The Upgrade Assistant helps you prepare for your upgrade to the next major {es} version. For example, if you are using 6.8, the Upgrade Assistant helps you to upgrade to 7.0. -To access the assistant, open the menu, then go to *Stack Management > Stack > Upgrade Assistant*. +To access the assistant, open the main menu, then click *Stack Management > Upgrade Assistant*. The assistant identifies the deprecated settings in your cluster and indices and guides you through the process of resolving issues, including reindexing. @@ -19,7 +19,7 @@ For example, if you want to upgrade to to 7.0, make sure that you are using 6.8. The `manage` cluster privilege is required to access the *Upgrade assistant*. Additional privileges may be needed to perform certain actions. -You can add this privilege in *Stack Management > Security > Roles*. +To add the privilege, open the main menu, then click *Stack Management > Roles*. [float] === Reindexing diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 23a0acbff5718..69c33aa7a1dac 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -8,8 +8,8 @@ Watches are helpful for analyzing mission-critical and business-critical streaming data. For example, you might watch application logs for performance outages or audit access logs for security threats. -To get started with the Watcher UI, open then menu, -then go to *Stack Management > Alerts and Insights > Watcher*. +To get started, open then main menu, +then click *Stack Management > Watcher*. With this UI, you can: * <> @@ -41,7 +41,7 @@ and either of these watcher roles: * `watcher_admin`. You can perform all Watcher actions, including create and edit watches. * `watcher_user`. You can view watches, but not create or edit them. -To manage roles, open then menu, then go to *Stack Management > Security > Roles*, or use the +To manage roles, open then main menu, then click *Stack Management > Roles*, or use the <>. Watches are shared between all users with the same role. diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc index 6c28840087252..3c9bea11176cc 100644 --- a/docs/maps/geojson-upload.asciidoc +++ b/docs/maps/geojson-upload.asciidoc @@ -19,7 +19,7 @@ GeoJSON is the most commonly used and flexible option. Follow these instructions to upload a GeoJSON data file, or try the <>. -. Open the menu, go to *Maps*, and then click *Add layer*. +. Open the main menu, click *Maps*, and then click *Add layer*. . Click *Uploaded GeoJSON*. + [role="screenshot"] diff --git a/docs/maps/images/fu_gs_select_source_file_upload.png b/docs/maps/images/fu_gs_select_source_file_upload.png deleted file mode 100644 index 4fe1162acb29c..0000000000000 Binary files a/docs/maps/images/fu_gs_select_source_file_upload.png and /dev/null differ diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc new file mode 100644 index 0000000000000..ff0c9bf1f72ba --- /dev/null +++ b/docs/maps/import-geospatial-data.asciidoc @@ -0,0 +1,46 @@ +[role="xpack"] +[[import-geospatial-data]] +== Import geospatial data + +To import geospatical data into the Elastic Stack, the data must be indexed as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. +Geospatial data comes in many formats. +Choose an import tool based on the format of your geospatial data. + +[discrete] +=== Upload CSV with latitude and longitude columns + +*File Data Visualizer* indexes CSV files with latitude and longitude columns as a geo_point. + +. Open the main menu, then click *Machine Learning*. +. Select the *Data Visualizer* tab, then click *Upload file*. +. Use the file chooser to select a CSV file. +. Click *Import*. +. Select the *Advanced* tab. +. Set *Index name*. +. Click *Add combined field*, then click *Add geo point field*. +. Fill out the form and click *Add*. +. Click *Import*. + +[discrete] +=== Upload a GeoJSON file + +*Upload GeoJSON* indexes GeoJSON features as a geo_point or geo_shape. + +. <>. +. Click *Add layer*. +. Select *Upload GeoJSON*. +. Use the file chooser to select a GeoJSON file. +. Click *Import file*. + +[discrete] +=== Upload data with IP addresses + +The GeoIP processor adds information about the geographical location of IP addresses. +See {ref}/geoip-processor.html[GeoIP processor] for details. +For private IP addresses, see https://www.elastic.co/blog/enriching-elasticsearch-data-geo-ips-internal-private-ip-addresses[Enriching data with GeoIPs from internal, private IP addresses]. + +[discrete] +=== Upload data with GDAL + +https://www.gdal.org/[GDAL] (Geospatial Data Abstraction Library) contains command line tools that can convert geospatial data between 75 different geospatial file formats and index that geospatial data into {es}. +See https://www.elastic.co/blog/how-to-ingest-geospatial-data-into-elasticsearch-with-gdal[Ingest geospatial data into Elasticsearch with GDAL] for details. diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index 8999b9fe20b11..3c3537826a6a8 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -56,6 +56,6 @@ include::maps-aggregations.asciidoc[] include::search.asciidoc[] include::map-settings.asciidoc[] include::connect-to-ems.asciidoc[] -include::geojson-upload.asciidoc[] +include::import-geospatial-data.asciidoc[] include::indexing-geojson-data-tutorial.asciidoc[] include::trouble-shooting.asciidoc[] diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index d1a6593f61fe1..434c9ab369a5b 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -47,7 +47,7 @@ image::maps/images/fu_gs_new_england_map.png[] For each GeoJSON file you downloaded, complete the following steps: . Click *Add layer*. -. From the list of layer types, click *Uploaded GeoJSON*. +. From the list of layer types, click *Upload GeoJSON*. . Using the File Picker, upload the GeoJSON file. + Depending on the geometry type of your features, this will diff --git a/docs/maps/map-settings.asciidoc b/docs/maps/map-settings.asciidoc index e11be438a2237..f606511a6f391 100644 --- a/docs/maps/map-settings.asciidoc +++ b/docs/maps/map-settings.asciidoc @@ -9,12 +9,16 @@ To access these settings, click *Map settings* in the application toolbar. [[maps-settings-navigation]] === Navigation +*Auto fit map to bounds*:: +When enabled, the map will automatically pan and zoom to show the filtered data bounds. + *Zoom range*:: Constrain the map to the defined zoom range. *Initial map location*:: Configure the initial map center and zoom. * *Map location at save*: Use the map center and zoom from the map position at the time of the latest save. +* *Auto fit map to bounds*: Set the initial map location to show the filtered data bounds. * *Fixed location*: Lock the map center and zoom to fixed values. * *Browser location*: Set the initial map center to the browser location. diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index e0d43a571a331..5c6cd87b235e1 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -50,7 +50,7 @@ In this tutorial, you'll learn to: The first thing to do is to create a new map. -. If you haven't already, open the menu, then click *{kib} > Maps*. +. If you haven't already, open the main menu, then click *Maps*. . On the maps list page, click *Create map*. . Set the time range to *Last 7 days*. + @@ -188,7 +188,7 @@ You have completed the steps for re-creating the sample data map. === Add the map to a dashboard You can add your saved map to a {kibana-ref}/dashboard.html[dashboard] and view your geospatial data alongside bar charts, pie charts, and other visualizations. -. Open the menu, then go to *Dashboard*. +. Open the main menu, then click *Dashboard*. . Click *Create dashboard*. . Set the time range to *Last 7 days*. . Click *Add*. diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 0c4042a37f700..09d9788cd37e0 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -49,6 +49,7 @@ Spatial filters have the following properties: * *Geometry label* enables you to provide a meaningful name for your spatial filter. * *Spatial field* specifies the geo_point or geo_shape field used to determine if a document matches the spatial relation with the specified geometry. * *Spatial relation* determines the {ref}/query-dsl-geo-shape-query.html#_spatial_relations[spatial relation operator] to use at search time. Only available when *Spatial field* is set to geo_shape. +* *Action* specifies whether to apply the filter to the current view or to a drilldown action. Only available when the map is a panel in a {kibana-ref}/dashboard.html[dashboard] with {kibana-ref}/drilldowns.html[drilldowns]. [float] [[maps-phrase-filter]] @@ -56,6 +57,7 @@ Spatial filters have the following properties: A phrase filter narrows search results to documents that contain the specified text. You can create a phrase filter by clicking the plus icon image:maps/images/gs_plus_icon.png[] in a <>. +If the map is a dashboard panel with drilldowns, you can apply a phrase filter to a drilldown by selecting the drilldown action. [role="screenshot"] image::maps/images/create_phrase_filter.png[] diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 494bd915b7f56..f7128b0f99f88 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -16,9 +16,20 @@ The index must contain at least one field mapped as {ref}/geo-point.html[geo_poi *Documents*:: Points, lines, and polyons from Elasticsearch. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. ++ +Results are limited to the `index.max_result_window` index setting, which defaults to 10000. +Select the appropriate *Scaling* option for your use case. ++ +* *Limit results to 10000.* The layer displays features from the first `index.max_result_window` documents. +Results exceeding `index.max_result_window` are not displayed. -NOTE: Document results are limited to the `index.max_result_window` index setting, which defaults to 10000. -Use <> to plot large data sets. +* *Show top hits per entity.* The layer displays the <>. + +* *Show clusters when results exceed 10000.* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents. + +* *Use vector tiles.* Vector tiles partition your map into 6 to 8 tiles. +Each tile request is limited to the `index.max_result_window` index setting. +Tiles exceeding `index.max_result_window` have a visual indicator when there are too many features to display. *EMS Boundaries*:: Administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index d8c200450d7e5..7fc8fe5114e1e 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -28,12 +28,12 @@ This page has moved. Please see the new section in the {heartbeat-ref}/securing- [role="exclude",id="infra-read-only-access"] == Configure source read-only access -This page has moved. Please see the new section in the {metrics-guide}/configure-metrics-source.html[Metrics Monitoring Guide]. +This page has moved. Please see {observability-guide}/configure-settings.html[configure settings]. [role="exclude",id="logs-read-only-access"] == Configure source read-only access -This page has moved. Please see {logs-guide}/configure-logs-source.html[logs configuration]. +This page has moved. Please see {observability-guide}/configure-data-sources.html[configure data sources]. [role="exclude",id="extend"] == Extend your use case diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index b396c40aa21f9..9054a97c90496 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -18,7 +18,7 @@ It is enabled by default. // Any changes made in this file will be seen there as well. // tag::apm-indices-settings[] -Index defaults can be changed in Kibana. Navigate to *APM* > *Settings* > *Indices*. +Index defaults can be changed in Kibana. Open the main menu, then click *APM > Settings > Indices*. Index settings in the APM app take precedence over those set in `kibana.yml`. [role="screenshot"] @@ -44,7 +44,7 @@ Changing these settings may disable features of the APM App. | Set to `false` to disable the APM app. Defaults to `true`. | `xpack.apm.ui.enabled` {ess-icon} - | Set to `false` to hide the APM app from the menu. Defaults to `true`. + | Set to `false` to hide the APM app from the main menu. Defaults to `true`. | `xpack.apm.ui.transactionGroupBucketSize` | Number of top transaction groups displayed in the APM app. Defaults to `1000`. diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 917821ad09e2f..f48dbeab9d61a 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -76,6 +76,9 @@ a|`monitoring.cluster_alerts.` health checks. By default, it matches the <> setting, which has a default value of `30000`. +| `monitoring.ui.elasticsearch.ssl` + | Shares the same configuration as <>. These settings configure encrypted communication between {kib} and the monitoring cluster. + |=== [float] diff --git a/docs/setup/access.asciidoc b/docs/setup/access.asciidoc index 49aa411e91512..edf936fe54267 100644 --- a/docs/setup/access.asciidoc +++ b/docs/setup/access.asciidoc @@ -1,24 +1,37 @@ [[access]] == Access {kib} -Kibana is a web application that you access through port 5601. All you need to do is point your web browser at the -machine where Kibana is running and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`. -If you want to allow remote users to connect, set the parameter `server.host` in `kibana.yml` to a non-loopback address. +The fastest way to access {kib} is to use our hosted {es} Service. If you <>, access {kib} through the web application. -When you access Kibana, the <> page loads by default with the default index pattern selected. The -time filter is set to the last 15 minutes and the search query is set to match-all (\*). +[float] +=== Set up on cloud -If you don't see any documents, try setting the time filter to a wider time range. -If you still don't see any results, it's possible that you don't *have* any documents. +include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] + +[float] +[[log-on-to-the-web-application]] +=== Log on to the web application + +If you are using a self-managed deployment, you access {kib} through the web application on port 5601. + +. Point your web browser to the machine where you are running {kib} and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`. + +. To allow remote users to connect to {kib}, set the parameter `server.host` in kibana.yml to a non-loopback address. + +. On the home page, click *{kib}*. ++ +To make the {kib} page your landing page, click *Make this my landing page*. [float] [[status]] -=== Check {kib} status +=== Check the {kib} status -You can reach the Kibana server's status page by navigating to the status endpoint, for example, `localhost:5601/status`. The status page displays -information about the server's resource usage and lists the installed plugins. +To view the {kib} status page, use the status endpoint. For example, `localhost:5601/status`. The status page displays +information about the server resource usage and installed plugins. [role="screenshot"] image::images/kibana-status-page-7_5_0.png[] -NOTE: For JSON-formatted server status details, use the API endpoint at `localhost:5601/api/status` +For JSON-formatted server status details, use the `localhost:5601/api/status` API endpoint. + + diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 0daa3f1e0e55e..c968ca6f35029 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -21,13 +21,14 @@ to see all that you can do in {kib}. experimental[] To visualize data in a CSV, JSON, or log file, you can upload it using the File -Data Visualizer. On the home page, click *Import a CSV, NDSON, or log file*, and -then drag your file into the File Data Visualizer. Alternatively, you can open +Data Visualizer. On the home page, click *Upload a file*, and +then drag your file onto the *File Data Visualizer*. Alternatively, you can open it by navigating to *Machine Learning* from the side navigation and selecting + *Data Visualizer*. [role="screenshot"] -image::images/data-viz-homepage.jpg[File Data Visualizer on the home page] +image::images/ingest-data.png[File Data Visualizer on the home page] You can upload a file up to 100 MB. This value is configurable up to 1 GB in <>. @@ -45,16 +46,12 @@ repeated production process, but rather for the initial exploration of your data [[upload-geoipdata-kibana]] === Upload geospatial data -To visualize geospatial data in a point or shape file, you can upload it using the <> +To visualize geospatial data in a point or shape file, you can upload it using the <> feature in Maps, and then use that data as a layer in a map. The data is also available for use in the broader Kibana ecosystem, for example, in visualizations and Canvas workpads. With GeoJSON Upload, you can upload a file up to 50 MB. -[role="screenshot"] -image::images/fu_gs_select_source_file_upload.png[] - - [float] [[add-data-tutorial-kibana]] === Index metrics, log, security, and application data @@ -82,7 +79,7 @@ create an index pattern that matches the names of the indices that you want to e When you add data with the File Data Visualizer, GeoJSON Upload feature, or built-in tutorial, an index pattern is created for you. -. Go to *Stack Management*, and then click *Index Patterns*. +. Open the main menu, then click *Stack Management > Index Patterns*. . Click *Create index pattern*. diff --git a/docs/setup/images/data-viz-homepage.jpg b/docs/setup/images/data-viz-homepage.jpg deleted file mode 100644 index f7a952b65d41f..0000000000000 Binary files a/docs/setup/images/data-viz-homepage.jpg and /dev/null differ diff --git a/docs/setup/images/ingest-data.png b/docs/setup/images/ingest-data.png new file mode 100644 index 0000000000000..b1943d6de27d2 Binary files /dev/null and b/docs/setup/images/ingest-data.png differ diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index 9e505b8bfe045..1bc781e1dda49 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -29,7 +29,7 @@ Kibana supports spaces in several ways. You can: [[spaces-managing]] === View, create, and delete spaces -Open the menu, then go to *Stack Management > {kib} > Spaces* for an overview of your spaces. This view provides actions +Open the main menu, then click *Stack Management > Spaces* for an overview of your spaces. This view provides actions for you to create, edit, and delete spaces. [role="screenshot"] @@ -94,8 +94,8 @@ image::spaces/images/spaces-roles.png["Controlling features visiblity"] [[spaces-moving-objects]] === Move saved objects between spaces -To <> from one space to another, open the menu, -then go to *Stack Management > {kib} > Saved objects*. +To <> from one space to another, open the main menu, +then click *Stack Management > Saved Objects*. Alternately, you can move objects using {kib}'s <> interface. diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index 9301224e6df48..aad192dbddb30 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -89,8 +89,8 @@ image::user/alerting/images/pagerduty-integration.png[PagerDuty Integrations tab + * Create a connector as part of creating an alert by selecting PagerDuty in the *Actions* section of the alert configuration and selecting *Add new*. -* Alternatively, create a connector by navigating to *Management* from the {kib} navbar and selecting -*Alerts and Actions*. Then, select the *Connectors* tab, click the *Create connector* button, and select the PagerDuty option. +* Alternatively, create a connector. To create a connector, open the main menu, click *Stack Management* > +Alerts and Actions*, select *Connectors*, click *Create connector*, then select the PagerDuty option. . Configure the connector by giving it a name and entering the Integration Key, optionally entering a custom API URL. + @@ -99,7 +99,7 @@ See <> for how to obtain the endpoint and . Save the Connector. -. Create an alert using *Management > Alerts and Actions* or the application of your choice. +. To create an alert, open the main menu, then click *Stack Management > Alerts and Actions* or the application of your choice. . Set up an action using your PagerDuty connector, by determining: + @@ -120,7 +120,7 @@ To remove a PagerDuty connector from an alert, simply remove it from the *Actions* section of that alert, using the remove (x) icon. This will disable the integration for the particular alert. -To delete the connector entirely, go to *Management > Alerts and Actions*. +To delete the connector entirely, open the main menu, then click *Stack Management > Alerts and Actions*. Select the *Connectors* tab, and then click on the delete icon. This is an irreversible action and impacts all alerts that use this connector. diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc index e3f1703f08e88..722607ac05f87 100644 --- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -61,7 +61,7 @@ Sensitive properties, such as passwords, can also be stored in the < {kib} > Alerts and Actions*, preconfigured connectors +When you open the main menu, click *Stack Management > Alerts and Actions*. Preconfigured connectors appear on the <>, regardless of which space you are in. They are tagged as “preconfigured”, and you cannot delete them. @@ -101,7 +101,7 @@ This example shows a preconfigured action type with one out-of-the box connector [[managing-pre-configured-action-types]] To attach a preconfigured action to an alert: -. Open the menu, then go to *Stack Management > {kib} > Alerts and Actions*, open the *Connectors* tab. +. Open the main menu, click *Stack Management > Alerts and Actions*, then open the *Connectors* tab. . Click *Create connector.* diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 297dfac5b10bd..c10641bb3a6b9 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -17,7 +17,7 @@ With Canvas, you can: * Focus the data you want to display with filters. -To begin, open the menu, then go to *Canvas*. +To begin, open the main menu, then click *Canvas*. [role="screenshot"] image::images/canvas-gs-example.png[Getting started example] diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc index 5e928fd731bb4..bdff7355d7467 100644 --- a/docs/user/dashboard/dashboard-drilldown.asciidoc +++ b/docs/user/dashboard/dashboard-drilldown.asciidoc @@ -57,8 +57,8 @@ TIP: If you don’t see data for a panel, try changing the time range. . Set a search and filter. + [%hardbreaks] -Search: `extension.keyword:( “gz” or “css” or “deb”)` -Filter: `geo.src : CN` +Search: `extension.keyword: ("gz" or "css" or "deb")` +Filter: `geo.src: CN` *Create the drilldown* @@ -94,4 +94,3 @@ image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to an + You are navigated to your destination dashboard. Verify that the search query, filters, and time range are carried over. - diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 4fa4f9860c2bd..5fda1af55c7fe 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -8,7 +8,7 @@ A _dashboard_ is a collection of panels that you use to analyze your data. On a you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations, interactive controls, markdown, and more. -With *Dashboard*s, you can: +With *Dashboard*, you can: * Add multiple panels to see many aspects and views of your data in one place. @@ -22,7 +22,7 @@ With *Dashboard*s, you can: * Generate reports based on your findings. -To begin, open the menu, go to *Dashboard*, then click *Create dashboard*. +To begin, open the main menu, click *Dashboard*, then click *Create dashboard*. [role="screenshot"] image:images/Dashboard_example.png[Example dashboard] @@ -424,33 +424,37 @@ Ready to try out Timelion? For step-by-step tutorials, refer to: [[timelion-deprecation]] ==== Timelion app deprecation -Deprecated since 7.0, the Timelion app will be removed in 8.0. If you have any Timelion worksheets, you must migrate them to a dashboard. +In 7.0 and later, *Timelion* app is deprecated. In 8.0 and later, *Timelion* app is removed from {kib}. To prepare for the removal of *Timelion* app, you must migrate *Timelion* app worksheets to a dashboard. -NOTE: Only the Timelion app is deprecated. {kib} continues to support Timelion -visualizations on dashboards and in Visualize and Canvas. +NOTE: Only *Timelion* app is deprecated. {kib} continues to support *Timelion* +visualizations in *Dashboard*, *Visualize*, and *Canvas*. -To migrate a Timelion worksheet to a dashboard: +To migrate a *Timelion* worksheet to a dashboard: -. Open the menu, click **Dashboard**, then click **Create dashboard**. +. Open the main menu, click *Dashboard*, then click *Create dashboard*. -. On the dashboard, click **Create New**, then select the Timelion visualization. +. For each *Timelion* app worksheet, complete the following steps. -. On a new tab, open the Timelion app, select the chart you want to copy, and copy its expression. +.. On the dashboard, click *Create New*, then click *Timelion* on the *New Visualization* window. + +.. Open a new tab, open the *Timelion* app, select the chart you want to copy, then copy the chart expression. + [role="screenshot"] -image::images/timelion-copy-expression.png[] +image::images/timelion-copy-expression.png[Timelion app chart] -. Return to the other tab and paste the copied expression to the *Timelion Expression* field and click **Update**. +.. Go to *Timelion*, paste the chart expression in the *Timelion expression* field, then click *Update*. + [role="screenshot"] -image::images/timelion-vis-paste-expression.png[] +image::images/timelion-vis-paste-expression.png[Timelion advanced editor UI] + +.. In the toolbar, click *Save*. -. Save the new visualization, give it a name, and click **Save and Return**. +.. On the *Save visualization* window, enter the visualization *Title*, then click *Save and return*. + -Your Timelion visualization will appear on the dashboard. Repeat this for all your charts on each worksheet. +The Timelion visualization panel appears on the dashboard. + [role="screenshot"] -image::images/timelion-dashboard.png[] +image::images/timelion-dashboard.png[Final dashboard with saved Timelion app worksheet] [float] [[save-panels]] @@ -458,7 +462,7 @@ image::images/timelion-dashboard.png[] When you’ve finished making changes, save the panels. -. Click *Save*. +. In the toolbar, click *Save*. . Add the *Title* and optional *Description*. . Click *Save and return*. diff --git a/docs/user/dashboard/edit-dashboards.asciidoc b/docs/user/dashboard/edit-dashboards.asciidoc index 7534ea1e9e9fb..7b712b355b315 100644 --- a/docs/user/dashboard/edit-dashboards.asciidoc +++ b/docs/user/dashboard/edit-dashboards.asciidoc @@ -78,7 +78,7 @@ Put the dashboard in *Edit* mode, then use the following options: * To resize, click the resize control, then drag to the new dimensions. -* To delete, open the panel menu, then select Delete from dashboard. When you delete a panel from the dashboard, the +* 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] diff --git a/docs/user/dashboard/images/Dashboard_add_new_visualization.png b/docs/user/dashboard/images/Dashboard_add_new_visualization.png index 3685f9c5c9a74..5f73b2f1adde2 100644 Binary files a/docs/user/dashboard/images/Dashboard_add_new_visualization.png and b/docs/user/dashboard/images/Dashboard_add_new_visualization.png differ diff --git a/docs/user/dashboard/images/Dashboard_add_visualization.png b/docs/user/dashboard/images/Dashboard_add_visualization.png index b1b86d47e5982..4caa34ef3d082 100644 Binary files a/docs/user/dashboard/images/Dashboard_add_visualization.png and b/docs/user/dashboard/images/Dashboard_add_visualization.png differ diff --git a/docs/user/dashboard/images/Dashboard_example.png b/docs/user/dashboard/images/Dashboard_example.png index 1a80f4b3bdf07..c2e338d0fd31b 100644 Binary files a/docs/user/dashboard/images/Dashboard_example.png and b/docs/user/dashboard/images/Dashboard_example.png differ diff --git a/docs/user/dashboard/images/Dashboard_inspect.png b/docs/user/dashboard/images/Dashboard_inspect.png index d65b968e043a6..635eef4a017f6 100644 Binary files a/docs/user/dashboard/images/Dashboard_inspect.png and b/docs/user/dashboard/images/Dashboard_inspect.png differ diff --git a/docs/user/dashboard/images/drilldown_on_piechart.gif b/docs/user/dashboard/images/drilldown_on_piechart.gif index c9b3311df0325..c438e14371887 100644 Binary files a/docs/user/dashboard/images/drilldown_on_piechart.gif and b/docs/user/dashboard/images/drilldown_on_piechart.gif differ diff --git a/docs/user/dashboard/images/timelion-copy-expression.png b/docs/user/dashboard/images/timelion-copy-expression.png new file mode 100644 index 0000000000000..a9c3afe9b060f Binary files /dev/null and b/docs/user/dashboard/images/timelion-copy-expression.png differ diff --git a/docs/visualize/images/timelion-vis-paste-expression.png b/docs/user/dashboard/images/timelion-vis-paste-expression.png similarity index 100% rename from docs/visualize/images/timelion-vis-paste-expression.png rename to docs/user/dashboard/images/timelion-vis-paste-expression.png diff --git a/docs/user/dashboard/images/url_drilldown_go_to_github.gif b/docs/user/dashboard/images/url_drilldown_go_to_github.gif index 7cca3f72d5a68..3a3b00dc0e2ce 100644 Binary files a/docs/user/dashboard/images/url_drilldown_go_to_github.gif and b/docs/user/dashboard/images/url_drilldown_go_to_github.gif differ diff --git a/docs/user/dashboard/share-dashboards.asciidoc b/docs/user/dashboard/share-dashboards.asciidoc index cfa146d60fdac..6c05240c934e8 100644 --- a/docs/user/dashboard/share-dashboards.asciidoc +++ b/docs/user/dashboard/share-dashboards.asciidoc @@ -23,5 +23,5 @@ tools. To create a short URL, you must have write access to {kib}. [[import-dashboards]] === Export the dashboard -To export the dashboard, open the menu, then click *Stack Management > Saved Objects*. For more information, +To export the dashboard, open the main menu, then click *Stack Management > Saved Objects*. For more information, refer to <>. \ No newline at end of file diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index b71dfb016c765..cdb17e9daa5e3 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -135,6 +135,92 @@ Example: `{{ date event.from “YYYY MM DD”}}` + `{{date “now-15”}}` + +|formatNumber +a|Format numbers. Numbers can be formatted to look like currency, percentages, times or numbers with decimal places, thousands, and abbreviations. +Refer to the http://numeraljs.com/#format[numeral.js] for different formatting options. + +Example: + +`{{formatNumber event.value "0.0"}}` + +|lowercase +a|Converts a string to lower case. + +Example: + +`{{lowercase event.value}}` + +|uppercase +a|Converts a string to upper case. + +Example: + +`{{uppercase event.value}}` + +|trim +a|Removes leading and trailing spaces from a string. + +Example: + +`{{trim event.value}}` + +|trimLeft +a|Removes leading spaces from a string. + +Example: + +`{{trimLeft event.value}}` + +|trimRight +a|Removes trailing spaces from a string. + +Example: + +`{{trimRight event.value}}` + +|mid +a|Extracts a substring from a string by start position and number of characters to extract. + +Example: + +`{{mid event.value 3 5}}` - extracts five characters starting from a third character. + +|left +a|Extracts a number of characters from a string (starting from left). + +Example: + +`{{left event.value 3}}` + +|right +a|Extracts a number of characters from a string (starting from right). + +Example: + +`{{right event.value 3}}` + +|concat +a|Concatenates two or more strings. + +Example: + +`{{concat event.value "," event.key}}` + +|replace +a|Replaces all substrings within a string. + +Example: + +`{{replace event.value "stringToReplace" "stringToReplaceWith"}}` + +|split +a|Splits a string using a provided splitter. + +Example: + +`{{split event.value ","}}` + |=== diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 378f7a53a6650..d6593143e4f6d 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -296,7 +296,8 @@ a configuration option for changing the tooltip position and padding: kibana: { tooltips: { position: 'top', - padding: 15 + padding: 15, + textTruncate: true, } } } diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index aca6d40a3532e..086c0707b3c2c 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -9,7 +9,7 @@ You must index data into {es} before you can create a graph. [[exploring-connections]] === Graph a data connection -. Open the menu, then go to *Graph*. +. Open the main menu, then click *Graph*. + If this is your first graph, follow the prompts to create it. For subsequent graphs, click *New*. diff --git a/docs/user/graph/images/graph-add-query.png b/docs/user/graph/images/graph-add-query.png index 1b233e3ef8b69..93ddf6a6132f4 100644 Binary files a/docs/user/graph/images/graph-add-query.png and b/docs/user/graph/images/graph-add-query.png differ diff --git a/docs/user/graph/images/graph-link-summary.png b/docs/user/graph/images/graph-link-summary.png index 4c75be00de0f5..a3dfdc0f79d96 100644 Binary files a/docs/user/graph/images/graph-link-summary.png and b/docs/user/graph/images/graph-link-summary.png differ diff --git a/docs/user/graph/images/graph-url-connections.png b/docs/user/graph/images/graph-url-connections.png index 4f8c163ab764b..34b57d489b048 100644 Binary files a/docs/user/graph/images/graph-url-connections.png and b/docs/user/graph/images/graph-url-connections.png differ diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 7e5dc59b03a2c..aa5b0ece08db7 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -20,16 +20,16 @@ and more — all from the convenience of a {kib} UI. document discovery to SIEM, {kib} is the portal for accessing these and other capabilities. [role="screenshot"] -image::images/intro-kibana.png[] +image::images/intro-kibana.png[Kibana home page] [float] [[get-data-into-kibana]] -=== Add data +=== Ingest data -{kib} is designed to use {es} as a data source. Think of Elasticsearch as the engine that stores +{kib} is designed to use {es} as a data source. Think of {es} as the engine that stores and processes the data, with {kib} sitting on top. -From the home page, {kib} provides these options for adding data: +From the home page, {kib} provides these options for ingesting data: * Import data using the https://www.elastic.co/blog/importing-csv-and-log-data-into-elasticsearch-with-file-data-visualizer[File Data visualizer]. @@ -60,7 +60,7 @@ search for hidden insights and relationships. Ask your questions, and then narrow the results to just the data you want. [role="screenshot"] -image::images/intro-discover.png[] +image::images/intro-discover.png[Discover UI] [float] [[visualize-and-analyze]] @@ -79,7 +79,7 @@ use <> to collect them in one place. A dashboard provides insights into your data from multiple perspectives. [role="screenshot"] -image::images/intro-dashboard.png[] +image::images/intro-dashboard.png[Sample eCommerce data set dashboard] {kib} also offers these visualization features: @@ -156,5 +156,4 @@ You can also <> — no code, no addi infrastructure required. Our <> and in-product guidance can -help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[] -in the top navigation bar for help with questions or to provide feedback. +help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[Help icon in navigation bar] for help with questions or to provide feedback. diff --git a/docs/user/introduction/images/intro-dashboard.png b/docs/user/introduction/images/intro-dashboard.png index fe4e6f620d19c..bb4e98a516fb7 100644 Binary files a/docs/user/introduction/images/intro-dashboard.png and b/docs/user/introduction/images/intro-dashboard.png differ diff --git a/docs/user/introduction/images/intro-data-tutorial.png b/docs/user/introduction/images/intro-data-tutorial.png index 2882a092fbb0b..781e134605b87 100644 Binary files a/docs/user/introduction/images/intro-data-tutorial.png and b/docs/user/introduction/images/intro-data-tutorial.png differ diff --git a/docs/user/introduction/images/intro-discover.png b/docs/user/introduction/images/intro-discover.png index 54e5725596421..134804941a356 100644 Binary files a/docs/user/introduction/images/intro-discover.png and b/docs/user/introduction/images/intro-discover.png differ diff --git a/docs/user/introduction/images/intro-kibana.png b/docs/user/introduction/images/intro-kibana.png index 62c2c99826131..3d10a31d7e380 100644 Binary files a/docs/user/introduction/images/intro-kibana.png and b/docs/user/introduction/images/intro-kibana.png differ diff --git a/docs/user/introduction/images/intro-spaces.png b/docs/user/introduction/images/intro-spaces.png new file mode 100644 index 0000000000000..6f3212cbde26e Binary files /dev/null and b/docs/user/introduction/images/intro-spaces.png differ diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 1ac5c385f8ed5..300497126c3e5 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -31,6 +31,33 @@ default, the trigger condition is set at 85% or more averaged over the last 5 minutes. The alert is grouped across all the nodes of the cluster by running checks on a schedule time of 1 minute with a re-notify internal of 1 day. +[discrete] +[[kibana-alerts-disk-usage-threshold]] +== Disk usage threshold + +This alert is triggered when a node is nearly at disk capacity. By +default, the trigger condition is set at 80% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-jvm-memory-threshold]] +== JVM memory threshold + +This alert is triggered when a node runs a consistently high JVM memory usage. By +default, the trigger condition is set at 85% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-missing-monitoring-data]] +== Missing monitoring data + +This alert is triggered when any stack products nodes or instances stop sending +monitoring data. By default, the trigger condition is set to missing for 15 minutes +looking back 1 day. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 6 hours. + NOTE: Some action types are subscription features, while others are free. For a comparison of the Elastic subscription levels, see the alerting section of the {subscriptions}[Subscriptions page]. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index 9d735ea1fe3db..047fcc08775e6 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -48,7 +48,7 @@ By default, if you are running {kib} locally, go to `http://localhost:5601/`. If {security-features} are enabled, log in. -- -... Open the menu, then go to *Stack Monitoring*. If data collection is +... Open the main menu, then click *Stack Monitoring*. If data collection is disabled, you are prompted to turn it on. ** From the Console or command line, set `xpack.monitoring.collection.enabled` diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 0c48e3b7d011d..9507b70c4f72e 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -80,7 +80,7 @@ By default, if you are running {kib} locally, go to `http://localhost:5601/`. If the Elastic {security-features} are enabled, log in. -- -. Open *Stack Monitoring*. +. Open the main menu, then click *Stack Monitoring*. + -- If data collection is disabled, you are prompted to turn on data collection. diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 371855deb2f3c..413573e7ec182 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -13,7 +13,7 @@ URL that triggers a report to generate. To create the POST URL for PDF reports: -. Go to *Dashboard*, then open the visualization or dashboard. +. Open then main menu, click *Dashboard*, then open a dashboard. + To specify a relative or absolute time period, use the time filter. diff --git a/docs/user/reporting/images/preserve-layout-switch.png b/docs/user/reporting/images/preserve-layout-switch.png index 9cfbdaafc3ac5..0aaefb14d7ee5 100644 Binary files a/docs/user/reporting/images/preserve-layout-switch.png and b/docs/user/reporting/images/preserve-layout-switch.png differ diff --git a/docs/user/reporting/images/share-button.png b/docs/user/reporting/images/share-button.png deleted file mode 100644 index 0b307d947935e..0000000000000 Binary files a/docs/user/reporting/images/share-button.png and /dev/null differ diff --git a/docs/user/reporting/images/share-menu.png b/docs/user/reporting/images/share-menu.png new file mode 100644 index 0000000000000..7f1d9eda0b5bc Binary files /dev/null and b/docs/user/reporting/images/share-menu.png differ diff --git a/docs/user/reporting/images/shareable-container.png b/docs/user/reporting/images/shareable-container.png index e114f63e2fe12..829fe15706a52 100644 Binary files a/docs/user/reporting/images/shareable-container.png and b/docs/user/reporting/images/shareable-container.png differ diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 50ae92382fb24..cd93389bb5fde 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -14,7 +14,7 @@ Reporting is available from the *Share* menu in *Discover*, *Dashboard*, and *Canvas*. [role="screenshot"] -image::user/reporting/images/share-button.png["Share"] +image::user/reporting/images/share-menu.png["Share"] [float] == Setup @@ -94,7 +94,7 @@ image::user/reporting/images/preserve-layout-switch.png["Share"] [[manage-report-history]] == View and manage report history -For a list of your reports, open the menu, then go to *Stack Management > Alerts and Insights > Reporting*. +For a list of your reports, open the main menu, then click *Stack Management > Reporting*. From this view, you can monitor the generation of a report and download reports that you previously generated. diff --git a/docs/user/security/api-keys/index.asciidoc b/docs/user/security/api-keys/index.asciidoc index 7cf1b964082d9..8b59115859622 100644 --- a/docs/user/security/api-keys/index.asciidoc +++ b/docs/user/security/api-keys/index.asciidoc @@ -15,7 +15,7 @@ Or, you might create API keys to automate ingestion of new data from remote sources, without a live user interaction. You can create API keys from the {kib} Console. To view and invalidate -API keys, open the menu, then go to *Stack Management > Security > API Keys*. +API keys, open the main menu, then click *Stack Management > API Keys*. [role="screenshot"] image:user/security/api-keys/images/api-keys.png["API Keys UI"] @@ -39,8 +39,8 @@ or contact your system administrator. === Security privileges You must have the `manage_security`, `manage_api_key`, or the `manage_own_api_key` -cluster privileges to use API keys in {kib}. To manage roles, open the menu, then go to -*Stack Management > Security > Roles*, or use the <>. +cluster privileges to use API keys in {kib}. To manage roles, open the main menu, then click +*Stack Management > Roles*, or use the <>. [float] diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 3af49753db664..150004b3ad691 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -12,7 +12,7 @@ NOTE: When running multiple tenants of {kib} by changing the `kibana.index` in y [[xpack-kibana-role-management]] === {kib} role management -To create a role that grants {kib} privileges, open the menu, go to *Stack Management > Security > Roles* and click **Create role**. +To create a role that grants {kib} privileges, open the main menu, click *Stack Management > Roles*, then click *Create role*. [[adding_kibana_privileges]] ==== Adding {kib} privileges diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index e1a46a415fe68..b5ab57d8f525a 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -13,7 +13,7 @@ auditing. For more information, see [float] === Users -To create and manage users, open the menu, then go to *Stack Management > Security > Users*. +To create and manage users, open the main menu, then click *Stack Management > Users*. You can also change their passwords and roles. For more information about authentication and built-in users, see {ref}/setting-up-authentication.html[Setting up user authentication]. @@ -21,7 +21,7 @@ authentication and built-in users, see [float] === Roles -To manage roles, open the menu, then go to *Stack Management > Security > Roles*, or use +To manage roles, open the main menu, then click *Stack Management > Roles*, or use the <>. For more information on configuring roles for {kib}, see <>. For a more holistic overview of configuring roles for the entire stack, diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index bf7be6284b1a9..2088110f6de21 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -45,7 +45,7 @@ through in this tutorial: [float] ==== Create a role -Open the menu, then go to *Stack Management > Security > Roles* +Open the main menu, then click *Stack Management > Roles* for an overview of your roles. This view provides actions for you to create, edit, and delete roles. @@ -90,7 +90,7 @@ image::security/images/role-space-visualization.png["Associate space"] [float] ==== Create the developer user account with the proper roles -. Open the menu, then go to *Stack Management > Security > Users*. +. Open the main menu, then click *Stack Management > Users*. . Click **Create user**, then give the user the `dev-mortgage` and `monitoring-user` roles, which are required for *Stack Monitoring* users. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index daf9720a0f1d8..6e7fc0c212f07 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -24,11 +24,11 @@ to report on and the {es} indices. [[reporting-roles-management-ui]] === If you are using the `native` realm -To assign roles, open the menu, then go to *Stack Management > Security > Roles*, use the <>. +To assign roles, use the *Roles* UI or <>. This example shows how to use *Roles* page to create a user who has a custom role and the `reporting_user` role. -. Open the menu, then go to *Stack Management > Security > Roles*. +. Open the main menu, then click *Stack Management > Roles*. . Click *Create role*, then give the role a name, for example, `custom_reporting_user`. @@ -51,7 +51,7 @@ that provides read and write privileges in . Save your new role. -. Open the menu, then go to *Stack Management > Security > Users*, add a new user, and assign the user the built-in +. Open the main menu, then click *Stack Management > Users*, add a new user, and assign the user the built-in `reporting_user` role and your new custom role, `custom_reporting_user`. [float] @@ -69,10 +69,10 @@ If you use a different pattern for the `xpack.reporting.index` setting, you must create a custom role with appropriate access to the index, similar to the following: -. Open the menu, then go to *Stack Management >Security > Roles*. +. Open the main menu, then click *Stack Management > Roles*. . Click *Create role*, then name the role `custom-reporting-user`. . Specify the custom index and assign it the `all` index privilege. -. Open the menu, then go to *Stack Management > Security > Users* and create a new user with +. Open the main menu, then click *Stack Management > Users* and create a new user with the `kibana_system` role and the `custom-reporting-user` role. . Configure {kib} to use the new account: [source,js] diff --git a/docs/user/security/role-mappings/index.asciidoc b/docs/user/security/role-mappings/index.asciidoc index 661c319af827f..3f9a17e98d77f 100644 --- a/docs/user/security/role-mappings/index.asciidoc +++ b/docs/user/security/role-mappings/index.asciidoc @@ -9,7 +9,7 @@ or SAML. Role mappings have no effect for users inside the `native` or `file` realms. -To manage your role mappings, open the menu, then go to *Stack Management > Security > Role Mappings*. +To manage your role mappings, open the main menu, then click *Stack Management > Role Mappings*. With *Role mappings*, you can: @@ -23,7 +23,7 @@ image:user/security/role-mappings/images/role-mappings-grid.png["Role mappings"] [float] === Create a role mapping -. Open the menu, then go to *Stack Management > Security > Role Mappings*. +. Open the main menu, then click *Stack Management > Role Mappings*. . Click *Create role mapping*. . Give your role mapping a unique name, and choose which roles you wish to assign to your users. + diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index e7bd297a3ebb5..613ec88ed0edc 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -81,10 +81,10 @@ use {kib}. For more information on Basic Authentication and additional methods of authenticating {kib} users, see <>. -To manage privileges, open the menu, then go to *Stack Management > Security > Roles*. +To manage privileges, open the main menu, then click *Stack Management > Roles*. -If you're using the native realm with Basic Authentication, open then menu, -then go to *Stack Management > Security > Users* to assign roles, or use the +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: diff --git a/docs/user/setup.asciidoc b/docs/user/setup.asciidoc index 31e7d157d1bc7..54bdfff8e0bbb 100644 --- a/docs/user/setup.asciidoc +++ b/docs/user/setup.asciidoc @@ -1,5 +1,5 @@ [[setup]] -= Set up Kibana += Set up [partintro] -- diff --git a/docs/visualize/images/timelion-copy-expression.png b/docs/visualize/images/timelion-copy-expression.png deleted file mode 100644 index 376bf7919166e..0000000000000 Binary files a/docs/visualize/images/timelion-copy-expression.png and /dev/null differ diff --git a/package.json b/package.json index 4e5d316bd4d8e..60f6d84ca579a 100644 --- a/package.json +++ b/package.json @@ -116,8 +116,8 @@ }, "dependencies": { "@elastic/datemath": "5.0.3", - "@elastic/elasticsearch": "7.9.1", - "@elastic/eui": "29.3.0", + "@elastic/elasticsearch": "7.10.0-rc.1", + "@elastic/eui": "29.5.0", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", @@ -136,6 +136,7 @@ "@kbn/std": "1.0.0", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/pdfmake": "^0.1.15", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", @@ -227,7 +228,7 @@ "@babel/register": "^7.10.5", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.6.1", - "@elastic/charts": "23.2.1", + "@elastic/charts": "24.0.0", "@elastic/ems-client": "7.10.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index 6ed3ae2eb2fb7..0f9b917e7f05a 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -12,7 +12,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/elasticsearch": "7.9.1", + "@elastic/elasticsearch": "7.10.0-rc.1", "@kbn/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/packages/kbn-monaco/src/esql/constants.ts b/packages/kbn-monaco/src/esql/constants.ts new file mode 100644 index 0000000000000..59bf9a94d05b2 --- /dev/null +++ b/packages/kbn-monaco/src/esql/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 ID = 'esql'; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts b/packages/kbn-monaco/src/esql/index.ts similarity index 72% rename from src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts rename to packages/kbn-monaco/src/esql/index.ts index 861017d99962e..b0e25af760a26 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts +++ b/packages/kbn-monaco/src/esql/index.ts @@ -17,11 +17,7 @@ * under the License. */ -import { get } from 'lodash'; -import { IIndexPattern } from '../../../../../../data/public'; +import { ID } from './constants'; +import { lexerRules } from './lexer_rules'; -export function getFieldFormat(indexPattern?: IIndexPattern, fieldName?: string): string { - return indexPattern && fieldName - ? get(indexPattern, ['fieldFormatMap', fieldName, 'type', 'title']) - : ''; -} +export const EsqlLang = { ID, lexerRules }; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts b/packages/kbn-monaco/src/esql/lexer_rules/esql.ts similarity index 98% rename from packages/kbn-monaco/src/xjson/lexer_rules/esql.ts rename to packages/kbn-monaco/src/esql/lexer_rules/esql.ts index e75b1013d3727..8badc8ffc4184 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts +++ b/packages/kbn-monaco/src/esql/lexer_rules/esql.ts @@ -17,9 +17,7 @@ * under the License. */ -import { monaco } from '../../monaco'; - -export const ID = 'esql'; +import { monaco } from '../../monaco_imports'; const brackets = [ { open: '[', close: ']', token: 'delimiter.square' }, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/index.ts b/packages/kbn-monaco/src/esql/lexer_rules/index.ts similarity index 93% rename from src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/index.ts rename to packages/kbn-monaco/src/esql/lexer_rules/index.ts index 9ab950fbfb2f2..5210bc2411716 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/index.ts +++ b/packages/kbn-monaco/src/esql/lexer_rules/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { getFieldFormat } from './get_field_format'; +export { lexerRules } from './esql'; diff --git a/packages/kbn-monaco/src/index.ts b/packages/kbn-monaco/src/index.ts index 9213a1bfe1327..2a8467d6ef8fd 100644 --- a/packages/kbn-monaco/src/index.ts +++ b/packages/kbn-monaco/src/index.ts @@ -17,8 +17,12 @@ * under the License. */ -export { monaco } from './monaco'; +// global setup for supported languages +import './register_globals'; + +export { monaco } from './monaco_imports'; export { XJsonLang } from './xjson'; +export { PainlessLang } from './painless'; /* eslint-disable-next-line @kbn/eslint/module_migration */ import * as BarePluginApi from 'monaco-editor/esm/vs/editor/editor.api'; diff --git a/packages/kbn-monaco/src/monaco.ts b/packages/kbn-monaco/src/monaco_imports.ts similarity index 100% rename from packages/kbn-monaco/src/monaco.ts rename to packages/kbn-monaco/src/monaco_imports.ts diff --git a/packages/kbn-monaco/src/painless/constants.ts b/packages/kbn-monaco/src/painless/constants.ts new file mode 100644 index 0000000000000..32bbc0aaaa0be --- /dev/null +++ b/packages/kbn-monaco/src/painless/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 ID = 'painless'; diff --git a/src/plugins/discover/public/application/angular/directives/index.js b/packages/kbn-monaco/src/painless/index.ts similarity index 62% rename from src/plugins/discover/public/application/angular/directives/index.js rename to packages/kbn-monaco/src/painless/index.ts index 5d8969a78f018..2ff1f4a19f9bd 100644 --- a/src/plugins/discover/public/application/angular/directives/index.js +++ b/packages/kbn-monaco/src/painless/index.ts @@ -17,15 +17,7 @@ * under the License. */ -import { DiscoverNoResults } from './no_results'; -import { DiscoverUninitialized } from './uninitialized'; -import { DiscoverHistogram } from './histogram'; -import { getAngularModule } from '../../../kibana_services'; +import { ID } from './constants'; +import { lexerRules } from './lexer_rules'; -const app = getAngularModule(); - -app.directive('discoverNoResults', (reactDirective) => reactDirective(DiscoverNoResults)); - -app.directive('discoverUninitialized', (reactDirective) => reactDirective(DiscoverUninitialized)); - -app.directive('discoverHistogram', (reactDirective) => reactDirective(DiscoverHistogram)); +export const PainlessLang = { ID, lexerRules }; diff --git a/packages/kbn-monaco/src/painless/lexer_rules/index.ts b/packages/kbn-monaco/src/painless/lexer_rules/index.ts new file mode 100644 index 0000000000000..7cf9064c6aa51 --- /dev/null +++ b/packages/kbn-monaco/src/painless/lexer_rules/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { lexerRules } from './painless'; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts similarity index 87% rename from packages/kbn-monaco/src/xjson/lexer_rules/painless.ts rename to packages/kbn-monaco/src/painless/lexer_rules/painless.ts index 676eb3134026a..2f4383911c9ad 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts +++ b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts @@ -17,16 +17,9 @@ * under the License. */ -import { monaco } from '../../monaco'; +import { monaco } from '../../monaco_imports'; -export const ID = 'painless'; - -/** - * Extends the default type for a Monarch language so we can use - * attribute references (like @keywords to reference the keywords list) - * in the defined tokenizer - */ -interface Language extends monaco.languages.IMonarchLanguage { +export interface Language extends monaco.languages.IMonarchLanguage { default: string; brackets: any; keywords: string[]; @@ -41,8 +34,7 @@ interface Language extends monaco.languages.IMonarchLanguage { } export const lexerRules = { - default: 'invalid', - tokenPostfix: '', + default: '', // painless does not use < >, so we define our own brackets: [ ['{', '}', 'delimiter.curly'], @@ -136,9 +128,9 @@ export const lexerRules = { }, ], // whitespace - [/[ \t\r\n]+/, { token: 'whitespace' }], + [/[ \t\r\n]+/, '@whitespace'], // comments - [/\/\*/, 'comment', '@comment'], + // [/\/\*/, 'comment', '@comment'], [/\/\/.*$/, 'comment'], // brackets [/[{}()\[\]]/, '@brackets'], @@ -168,7 +160,6 @@ export const lexerRules = { // strings single quoted [/'([^'\\]|\\.)*$/, 'string.invalid'], // string without termination [/'/, 'string', '@string_sq'], - [/"""/, { token: 'punctuation.end_triple_quote', nextEmbedded: '@pop' }], ], comment: [ [/[^\/*]+/, 'comment'], @@ -189,6 +180,3 @@ export const lexerRules = { ], }, } as Language; - -monaco.languages.register({ id: ID }); -monaco.languages.setMonarchTokensProvider(ID, lexerRules); diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts new file mode 100644 index 0000000000000..b9e94803b7542 --- /dev/null +++ b/packages/kbn-monaco/src/register_globals.ts @@ -0,0 +1,55 @@ +/* + * 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 { XJsonLang } from './xjson'; +import { PainlessLang } from './painless'; +import { EsqlLang } from './esql'; +import { monaco } from './monaco_imports'; +// @ts-ignore +import xJsonWorkerSrc from '!!raw-loader!../target/public/xjson.editor.worker.js'; +// @ts-ignore +import defaultWorkerSrc from '!!raw-loader!../target/public/default.editor.worker.js'; + +/** + * Register languages and lexer rules + */ +monaco.languages.register({ id: XJsonLang.ID }); +monaco.languages.setMonarchTokensProvider(XJsonLang.ID, XJsonLang.lexerRules); +monaco.languages.setLanguageConfiguration(XJsonLang.ID, XJsonLang.languageConfiguration); +monaco.languages.register({ id: PainlessLang.ID }); +monaco.languages.setMonarchTokensProvider(PainlessLang.ID, PainlessLang.lexerRules); +monaco.languages.register({ id: EsqlLang.ID }); +monaco.languages.setMonarchTokensProvider(EsqlLang.ID, EsqlLang.lexerRules); + +/** + * Create web workers by language ID + */ +const mapLanguageIdToWorker: { [key: string]: any } = { + [XJsonLang.ID]: xJsonWorkerSrc, +}; + +// @ts-ignore +window.MonacoEnvironment = { + getWorker: (module: string, languageId: string) => { + const workerSrc = mapLanguageIdToWorker[languageId] || defaultWorkerSrc; + + const blob = new Blob([workerSrc], { type: 'application/javascript' }); + return new Worker(URL.createObjectURL(blob)); + }, +}; diff --git a/packages/kbn-monaco/src/xjson/index.ts b/packages/kbn-monaco/src/xjson/index.ts index 8a4644a3792d2..c372f02c09c76 100644 --- a/packages/kbn-monaco/src/xjson/index.ts +++ b/packages/kbn-monaco/src/xjson/index.ts @@ -22,5 +22,6 @@ */ import './language'; import { ID } from './constants'; +import { lexerRules, languageConfiguration } from './lexer_rules'; -export const XJsonLang = { ID }; +export const XJsonLang = { ID, lexerRules, languageConfiguration }; diff --git a/packages/kbn-monaco/src/xjson/language.ts b/packages/kbn-monaco/src/xjson/language.ts index 4ae7f2402ed2f..9759dc1b24401 100644 --- a/packages/kbn-monaco/src/xjson/language.ts +++ b/packages/kbn-monaco/src/xjson/language.ts @@ -19,32 +19,12 @@ // This file contains a lot of single setup logic for registering a language globally -import { monaco } from '../monaco'; +import { monaco } from '../monaco_imports'; import { WorkerProxyService } from './worker_proxy_service'; -import { registerLexerRules } from './lexer_rules'; import { ID } from './constants'; -// @ts-ignore -import workerSrc from '!!raw-loader!../../target/public/xjson.editor.worker.js'; const wps = new WorkerProxyService(); -// Register rules against shared monaco instance. -registerLexerRules(monaco); - -// In future we will need to make this map languages to workers using "id" and/or "label" values -// that get passed in. Also this should not live inside the "xjson" dir directly. We can update this -// once we have another worker. -// @ts-ignore -window.MonacoEnvironment = { - getWorker: (module: string, languageId: string) => { - if (languageId === ID) { - // In kibana we will probably build this once and then load with raw-loader - const blob = new Blob([workerSrc], { type: 'application/javascript' }); - return new Worker(URL.createObjectURL(blob)); - } - }, -}; - monaco.languages.onLanguage(ID, async () => { return wps.setup(); }); diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/index.ts b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts index 515de09510a61..7393c6a68c1bf 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/index.ts +++ b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts @@ -17,17 +17,4 @@ * under the License. */ -/* eslint-disable-next-line @kbn/eslint/module_migration */ -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import * as xJson from './xjson'; -import * as esql from './esql'; -import * as painless from './painless'; - -export const registerLexerRules = (m: typeof monaco) => { - m.languages.register({ id: xJson.ID }); - m.languages.setMonarchTokensProvider(xJson.ID, xJson.lexerRules); - m.languages.register({ id: painless.ID }); - m.languages.setMonarchTokensProvider(painless.ID, painless.lexerRules); - m.languages.register({ id: esql.ID }); - m.languages.setMonarchTokensProvider(esql.ID, esql.lexerRules); -}; +export { lexerRules, languageConfiguration } from './xjson'; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts index d6fea9e91acfb..e0c566fd3b0f2 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts +++ b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts @@ -17,15 +17,10 @@ * under the License. */ -import { monaco } from '../../monaco'; -import { ID } from '../constants'; -import './painless'; -import './esql'; +import { monaco } from '../../monaco_imports'; import { globals } from './shared'; -export { ID }; - export const lexerRules: monaco.languages.IMonarchLanguage = { ...(globals as any), @@ -124,11 +119,7 @@ export const lexerRules: monaco.languages.IMonarchLanguage = { }, }; -monaco.languages.register({ - id: ID, -}); -monaco.languages.setMonarchTokensProvider(ID, lexerRules); -monaco.languages.setLanguageConfiguration(ID, { +export const languageConfiguration: monaco.languages.LanguageConfiguration = { brackets: [ ['{', '}'], ['[', ']'], @@ -138,4 +129,4 @@ monaco.languages.setLanguageConfiguration(ID, { { open: '[', close: ']' }, { open: '"', close: '"' }, ], -}); +}; diff --git a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts index 548a413a483d9..c0e735b294484 100644 --- a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts +++ b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts @@ -18,7 +18,7 @@ */ import { ParseResult } from './grammar'; -import { monaco } from '../monaco'; +import { monaco } from '../monaco_imports'; import { XJsonWorker } from './worker'; import { ID } from './constants'; diff --git a/packages/kbn-monaco/webpack.config.js b/packages/kbn-monaco/webpack.config.js index 1a7d8c031670c..53f440689a233 100644 --- a/packages/kbn-monaco/webpack.config.js +++ b/packages/kbn-monaco/webpack.config.js @@ -19,33 +19,40 @@ const path = require('path'); -const createLangWorkerConfig = (lang) => ({ - mode: 'production', - entry: path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`), - output: { - path: path.resolve(__dirname, 'target/public'), - filename: `${lang}.editor.worker.js`, - }, - resolve: { - modules: ['node_modules'], - extensions: ['.js', '.ts', '.tsx'], - }, - stats: 'errors-only', - module: { - rules: [ - { - test: /\.(js|ts)$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], +const createLangWorkerConfig = (lang) => { + const entry = + lang === 'default' + ? 'monaco-editor/esm/vs/editor/editor.worker.js' + : path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`); + + return { + mode: 'production', + entry, + output: { + path: path.resolve(__dirname, 'target/public'), + filename: `${lang}.editor.worker.js`, + }, + resolve: { + modules: ['node_modules'], + extensions: ['.js', '.ts', '.tsx'], + }, + stats: 'errors-only', + module: { + rules: [ + { + test: /\.(js|ts)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, }, }, - }, - ], - }, -}); + ], + }, + }; +}; -module.exports = [createLangWorkerConfig('xjson')]; +module.exports = [createLangWorkerConfig('xjson'), createLangWorkerConfig('default')]; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index b075a678bff38..fd0be15affab3 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -68,7 +68,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 189428 securityOss: 30806 - securitySolution: 622387 + securitySolution: 283440 share: 99061 snapshotRestore: 79032 spaces: 387915 diff --git a/packages/kbn-plugin-generator/README.md b/packages/kbn-plugin-generator/README.md index 9ff9a8aa95ca2..bee8e6c2ca783 100644 --- a/packages/kbn-plugin-generator/README.md +++ b/packages/kbn-plugin-generator/README.md @@ -51,7 +51,7 @@ yarn kbn bootstrap Generated plugins receive a handful of scripts that can be used during development. Those scripts are detailed in the [README.md](template/README.md) file in each newly generated plugin, and expose the scripts provided by the [Kibana plugin helpers](../kbn-plugin-helpers), but here is a quick reference in case you need it: -> ***NOTE:*** All of these scripts should be run from the generated plugin. +> ***NOTE:*** The following scripts should be run from the generated plugin. - `yarn kbn bootstrap` @@ -59,14 +59,6 @@ Generated plugins receive a handful of scripts that can be used during developme > ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change. - - `yarn start` - - Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana` - - ``` - yarn start --elasticsearch.hosts http://localhost:9220 - ``` - - `yarn build` Build a distributable archive of your plugin. @@ -75,4 +67,15 @@ Generated plugins receive a handful of scripts that can be used during developme Run the server tests using mocha. + +To start kibana run the following command from Kibana root. + + - `yarn start` + + Start kibana and it will automatically include this plugin. You can pass any arguments that you would normally send to `bin/kibana` + + ``` + yarn start --elasticsearch.hosts http://localhost:9220 + ``` + For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks run `yarn run` or take a look in the `package.json` file. diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index a2c4e1e2134e7..1f86122d7e129 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -4,6 +4,9 @@ "private": true, "description": "Just some helpers for kibana plugin devs.", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "main": "target/index.js", "bin": { "plugin-helpers": "bin/plugin-helpers.js" diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index 557f38ec740fc..9684201c72384 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -12,6 +12,9 @@ }, "author": "", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "bugs": { "url": "https://github.com/jbudz/spec-to-console/issues" }, diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index dc2647b7b5757..c3bc65059d4a6 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -20,12 +20,7 @@ import { StorybookConfig } from '@storybook/core/types'; export const defaultConfig: StorybookConfig = { - addons: [ - '@kbn/storybook/preset', - '@storybook/addon-a11y', - '@storybook/addon-knobs', - '@storybook/addon-essentials', - ], + addons: ['@kbn/storybook/preset', '@storybook/addon-a11y', '@storybook/addon-essentials'], stories: ['../**/*.stories.tsx'], typescript: { reactDocgen: false, diff --git a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts index 1390c44d84a07..a95215a0044f1 100644 --- a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts +++ b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts @@ -24,13 +24,13 @@ import { readFileSync } from 'fs'; import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { REPO_ROOT } from '@kbn/utils'; const MINUTE = 1000 * 60; const FIXTURE_DIR = resolve(__dirname, '__fixtures__'); const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); -const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); +const XML_PATH = getUniqueJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); afterAll(async () => { await del(TARGET_DIR); diff --git a/packages/kbn-test/src/jest/junit_reporter.ts b/packages/kbn-test/src/jest/junit_reporter.ts index 0712584122e05..b6e964c22adfc 100644 --- a/packages/kbn-test/src/jest/junit_reporter.ts +++ b/packages/kbn-test/src/jest/junit_reporter.ts @@ -27,7 +27,7 @@ import type { Config } from '@jest/types'; import { AggregatedResult, Test, BaseReporter } from '@jest/reporters'; import { escapeCdata } from '../mocha/xml'; -import { makeJunitReportPath } from './report_path'; +import { getUniqueJunitReportPath } from './report_path'; interface ReporterOptions { reportName?: string; @@ -115,7 +115,7 @@ export default class JestJUnitReporter extends BaseReporter { }); }); - const reportPath = makeJunitReportPath(rootDirectory, reportName); + const reportPath = getUniqueJunitReportPath(rootDirectory, reportName); const reportXML = root.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/packages/kbn-test/src/jest/report_path.ts b/packages/kbn-test/src/jest/report_path.ts index fe122c349c193..c9cf3ce454e6a 100644 --- a/packages/kbn-test/src/jest/report_path.ts +++ b/packages/kbn-test/src/jest/report_path.ts @@ -17,14 +17,24 @@ * under the License. */ -import { resolve } from 'path'; +import Fs from 'fs'; +import Path from 'path'; + import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; -export function makeJunitReportPath(rootDirectory: string, reportName: string) { - return resolve( +export function getUniqueJunitReportPath( + rootDirectory: string, + reportName: string, + counter?: number +): string { + const path = Path.resolve( rootDirectory, 'target/junit', process.env.JOB || '.', - `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}.xml` + `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}${counter ? `-${counter}` : ''}.xml` ); + + return Fs.existsSync(path) + ? getUniqueJunitReportPath(rootDirectory, reportName, (counter ?? 0) + 1) + : path; } diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index 00a11432dd9e8..dc7d161eca5a3 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -25,13 +25,14 @@ import { parseString } from 'xml2js'; import del from 'del'; import Mocha from 'mocha'; import expect from '@kbn/expect'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { setupJUnitReportGeneration } from '../junit_report_generation'; const PROJECT_DIR = resolve(__dirname, 'fixtures/project'); const DURATION_REGEX = /^\d+\.\d{3}$/; const ISO_DATE_SEC_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/; +const XML_PATH = getUniqueJunitReportPath(PROJECT_DIR, 'test'); describe('dev/mocha/junit report generation', () => { afterEach(() => { @@ -50,9 +51,7 @@ describe('dev/mocha/junit report generation', () => { mocha.addFile(resolve(PROJECT_DIR, 'test.js')); await new Promise((resolve) => mocha.run(resolve)); - const report = await fcb((cb) => - parseString(readFileSync(makeJunitReportPath(PROJECT_DIR, 'test')), cb) - ); + const report = await fcb((cb) => parseString(readFileSync(XML_PATH), cb)); // test case results are wrapped in expect(report).to.eql({ diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 7e39c32ee4db8..9ac9bd18548f4 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -22,7 +22,7 @@ import { writeFileSync, mkdirSync } from 'fs'; import { inspect } from 'util'; import xmlBuilder from 'xmlbuilder'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { getSnapshotOfRunnableLogs } from './log_cache'; import { escapeCdata } from '../'; @@ -140,7 +140,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { } }); - const reportPath = makeJunitReportPath(rootDirectory, reportName); + const reportPath = getUniqueJunitReportPath(rootDirectory, reportName); const reportXML = builder.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 21d25311420ca..d954ae0823caf 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@babel/core": "^7.11.6", - "@elastic/eui": "29.3.0", + "@elastic/eui": "29.5.0", "@kbn/babel-preset": "1.0.0", "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 0a154c537fec1..b1b5d6e2b419e 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,8 +9,8 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "23.2.1", - "@elastic/eui": "29.3.0", + "@elastic/charts": "24.0.0", + "@elastic/eui": "29.5.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", diff --git a/renovate.json5 b/renovate.json5 index 17391c2f83827..84f8da2a72456 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -2,9 +2,12 @@ extends: [ 'config:base', ], - includePaths: [ - 'package.json', - 'x-pack/package.json', + ignorePaths: [ + '**/__fixtures__/**', + '**/fixtures/**', + ], + enabledManagers: [ + 'npm', ], baseBranches: [ 'master', diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 6a21dcb1b0686..49b962670220c 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -966,7 +966,7 @@ It means that NP plugin artifacts tend to have a bigger size than the legacy pla To understand the current size of your plugin artifact, run `@kbn/optimizer` as ```bash -node scripts/build_kibana_platform_plugins.js --dist --no-examples +node scripts/build_kibana_platform_plugins.js --dist --profile --focus=my_plugin ``` and check the output in the `target` sub-folder of your plugin folder diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index d982136422268..8ae559742c0da 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -107,7 +107,7 @@ describe('AppRouter', () => { expect(app1.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -119,7 +119,7 @@ describe('AppRouter', () => { expect(app1Unmount).toHaveBeenCalled(); expect(app2.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app2 html:
App 2
" @@ -133,7 +133,7 @@ describe('AppRouter', () => { expect(standardApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -145,7 +145,7 @@ describe('AppRouter', () => { expect(standardAppUnmount).toHaveBeenCalled(); expect(chromelessApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -157,7 +157,7 @@ describe('AppRouter', () => { expect(chromelessAppUnmount).toHaveBeenCalled(); expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -171,7 +171,7 @@ describe('AppRouter', () => { expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -183,7 +183,7 @@ describe('AppRouter', () => { expect(chromelessAppAUnmount).toHaveBeenCalled(); expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-b/path html:
Chromeless B
" @@ -195,7 +195,7 @@ describe('AppRouter', () => { expect(chromelessAppBUnmount).toHaveBeenCalled(); expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 47b8aaefaf86a..cf734f33cc3e4 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -585,7 +585,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiListGroupItem__icon" type="home" > -
@@ -671,7 +671,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` size="m" type="arrowRight" > -
-
-
@@ -1215,7 +1215,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` size="m" type="arrowRight" > -
-
@@ -1466,7 +1466,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` size="m" type="arrowRight" > -
-
@@ -1678,7 +1678,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` size="m" type="arrowRight" > -
-
@@ -1900,7 +1900,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="collapsible-nav-lock" type="button" > -
@@ -1935,7 +1935,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiListGroupItem__icon" type="lockOpen" > -
@@ -1994,7 +1994,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` size="m" type="cross" > -
-
@@ -2916,7 +2916,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` size="m" type="arrowRight" > -
-
@@ -3088,7 +3088,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` className="euiListGroupItem__icon" type="lock" > -
@@ -3147,7 +3147,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` size="m" type="cross" > -
-
-
@@ -4863,7 +4863,7 @@ exports[`Header renders 1`] = ` size="m" type="menu" > -
@@ -5694,7 +5694,7 @@ exports[`Header renders 1`] = ` className="euiListGroupItem__icon" type="home" > -
@@ -5780,7 +5780,7 @@ exports[`Header renders 1`] = ` size="m" type="arrowRight" > -
-
@@ -6037,7 +6037,7 @@ exports[`Header renders 1`] = ` className="euiListGroupItem__icon" type="lock" > -
@@ -6096,7 +6096,7 @@ exports[`Header renders 1`] = ` size="m" type="cross" > -
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index aea52eb8e7ab7..7e79725c20307 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -31,7 +31,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
Modal content
"`; +exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
Modal content
"`; exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ @@ -53,7 +53,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

Some message

"`; +exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

Some message

"`; exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ @@ -145,7 +145,7 @@ Array [ ] `; -exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; +exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ diff --git a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts index 340f45a0a2c18..b7ffefe7005e1 100644 --- a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts +++ b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts @@ -24,7 +24,8 @@ const { startES } = kbnTestServer.createTestServers({ }); let esServer: kbnTestServer.TestElasticsearchUtils; -describe('default route provider', () => { +// FLAKY: https://github.com/elastic/kibana/issues/81072 +describe.skip('default route provider', () => { let root: Root; beforeAll(async () => { diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 71900ab982f3d..b70680594151b 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -103,6 +103,10 @@ interface ListenerOptions { export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) { const server = new Server(serverOptions); + // remove fix + test as soon as update node.js to v12.19 https://github.com/elastic/kibana/pull/61587 + server.listener.headersTimeout = + listenerOptions.keepaliveTimeout + 2 * server.listener.headersTimeout; + server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; server.listener.setTimeout(listenerOptions.socketTimeout); server.listener.on('timeout', (socket) => { diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 59090d101acbc..01817b29de8ac 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -117,6 +117,62 @@ describe('OnPreRouting', () => { expect(urlAfterForwarding).toBe('/redirectUrl'); }); + it('provides original request url', async () => { + const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/login', validate: false }, (context, req, res) => { + return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + }); + + registerOnPreRouting((req, res, t) => t.rewriteUrl('/login')); + + await server.start(); + + await supertest(innerServer.listener) + .get('/initial?name=foo') + .expect(200, { rewrittenUrl: '/initial?name=foo' }); + }); + + it('provides original request url if rewritten several times', async () => { + const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/reroute-2', validate: false }, (context, req, res) => { + return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + }); + + registerOnPreRouting((req, res, t) => t.rewriteUrl('/reroute-1')); + registerOnPreRouting((req, res, t) => t.rewriteUrl('/reroute-2')); + + await server.start(); + + await supertest(innerServer.listener) + .get('/initial?name=foo') + .expect(200, { rewrittenUrl: '/initial?name=foo' }); + }); + + it('does not provide request url if interceptor does not rewrite url', async () => { + const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/login', validate: false }, (context, req, res) => { + return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + }); + + registerOnPreRouting((req, res, t) => t.next()); + + await server.start(); + + await supertest(innerServer.listener).get('/login').expect(200, {}); + }); + it('supports redirection from the interceptor', async () => { const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup( setupDeps diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 0170e94867c06..3d0eba6de632e 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -313,7 +313,6 @@ describe('KibanaRequest', () => { expect(resp3.body).toEqual({ requestId: 'gamma' }); }); }); - describe('request uuid', () => { it('generates a UUID', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); diff --git a/src/core/server/http/lifecycle/on_pre_routing.ts b/src/core/server/http/lifecycle/on_pre_routing.ts index e62eb54f2398f..92ae1f0b7bbdf 100644 --- a/src/core/server/http/lifecycle/on_pre_routing.ts +++ b/src/core/server/http/lifecycle/on_pre_routing.ts @@ -25,6 +25,7 @@ import { KibanaResponse, lifecycleResponseFactory, LifecycleResponseFactory, + KibanaRequestState, } from '../router'; enum ResultType { @@ -108,6 +109,9 @@ export function adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) { } if (preRoutingResult.isRewriteUrl(result)) { + const appState = request.app as KibanaRequestState; + appState.rewrittenUrl = appState.rewrittenUrl ?? request.url; + const { url } = result; request.setUrl(url); // We should update raw request as well since it can be proxied to the old platform diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 903eb75022df3..2d0e8d6c1a6ad 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -45,6 +45,7 @@ export interface KibanaRouteOptions extends RouteOptionsApp { export interface KibanaRequestState extends ApplicationState { requestId: string; requestUuid: string; + rewrittenUrl?: Url; } /** @@ -186,6 +187,11 @@ export class KibanaRequest< isAuthenticated: boolean; }; + /** + * URL rewritten in onPreRouting request interceptor. + */ + public readonly rewrittenUrl?: Url; + /** @internal */ protected readonly [requestSymbol]: Request; @@ -199,10 +205,12 @@ export class KibanaRequest< private readonly withoutSecretHeaders: boolean ) { // The `requestId` and `requestUuid` properties will not be populated for requests that are 'faked' by internal systems that leverage - // KibanaRequest in conjunction with scoped Elaticcsearch and SavedObjectsClient in order to pass credentials. + // KibanaRequest in conjunction with scoped Elasticsearch and SavedObjectsClient in order to pass credentials. // In these cases, the ids default to a newly generated UUID. - this.id = (request.app as KibanaRequestState | undefined)?.requestId ?? uuid.v4(); - this.uuid = (request.app as KibanaRequestState | undefined)?.requestUuid ?? uuid.v4(); + const appState = request.app as KibanaRequestState | undefined; + this.id = appState?.requestId ?? uuid.v4(); + this.uuid = appState?.requestUuid ?? uuid.v4(); + this.rewrittenUrl = appState?.rewrittenUrl; this.url = request.url; this.headers = deepFreeze({ ...request.headers }); diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index b4d91926f13f4..412396644648e 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -50,6 +50,8 @@ configService.atPath.mockReturnValue( allowFromAnyIp: true, ipAllowlist: [], }, + keepaliveTimeout: 120_000, + socketTimeout: 120_000, } as any) ); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7cd8682050e68..d9dc46d2cad99 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1007,6 +1007,7 @@ export class KibanaRequest>; // (undocumented) readonly socket: IKibanaSocket; 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 f5cf6c85fcbef..274d7a4e5a488 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 @@ -138,6 +138,7 @@ kibana_vars=( tilemap.url timelion.enabled vega.enableExternalUrls + xpack.actions.proxyUrl xpack.apm.enabled xpack.apm.serviceMapEnabled xpack.apm.ui.enabled diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index e7523c1bf6032..bd30efcb1c6d3 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -33,7 +33,6 @@ RUN tar --strip-components=1 -zxf /opt/{{artifactTarball}} # OpenShift does this, for example. # REF: https://docs.openshift.org/latest/creating_images/guidelines.html RUN chmod -R g=u /usr/share/kibana -RUN find /usr/share/kibana -type d -exec chmod g+s {} \; ################################################################################ # Build stage 1 (the actual Kibana image): @@ -54,9 +53,15 @@ RUN for iter in {1..10}; do \ (exit $exit_code) # Add an init process, check the checksum to make sure it's a match -RUN curl -L -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 -RUN echo "37f2c1f0372a45554f1b89924fbb134fc24c3756efaedf11e07f599494e0eff9 /usr/local/bin/dumb-init" | sha256sum -c - -RUN chmod +x /usr/local/bin/dumb-init +RUN set -e ; \ + TINI_VERSION='v0.19.0' ; \ + TINI_BIN='tini-amd64' ; \ + curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}" ; \ + curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}.sha256sum" ; \ + sha256sum -c "${TINI_BIN}.sha256sum" ; \ + rm "${TINI_BIN}.sha256sum" ; \ + mv "${TINI_BIN}" /bin/tini ; \ + chmod +x /bin/tini RUN mkdir /usr/share/fonts/local RUN curl -L -o /usr/share/fonts/local/NotoSansCJK-Regular.ttc https://github.com/googlefonts/noto-cjk/raw/NotoSansV2.001/NotoSansCJK-Regular.ttc @@ -126,6 +131,6 @@ RUN mkdir /licenses && \ USER kibana -ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] +ENTRYPOINT ["/bin/tini", "--"] CMD ["/usr/local/bin/kibana-docker"] diff --git a/src/legacy/server/i18n/index.ts b/src/legacy/server/i18n/index.ts index cb86c3220bec1..61caefb2fb599 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/legacy/server/i18n/index.ts @@ -33,7 +33,7 @@ export async function i18nMixin(kbnServer: KbnServer, server: Server, config: Ki const translationPaths = await Promise.all([ getTranslationPaths({ cwd: fromRoot('.'), - glob: I18N_RC, + glob: `*/${I18N_RC}`, }), ...(config.get('plugins.paths') as string[]).map((cwd) => getTranslationPaths({ cwd, glob: I18N_RC }) diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 794168132abb2..e9fa2833c3db5 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -22,6 +22,7 @@ import classNames from 'classnames'; import 'brace/theme/textmate'; import 'brace/mode/markdown'; +import 'brace/mode/json'; import { EuiBadge, diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 201c6e83a4a44..d523bd2ea5b52 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -704,7 +704,7 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` size="m" type="plusInCircle" > -
{ + const field = getField({ type: 'unknown' }); + expect(field.visualizable).toEqual(false); + + const fieldB = getField({ type: 'conflict' }); + expect(fieldB.visualizable).toEqual(false); + + const fieldC = getField({ aggregatable: false, scripted: false }); + expect(fieldC.visualizable).toEqual(false); + }); + it('calculates aggregatable', () => { const field = getField({ aggregatable: true, scripted: false }); expect(field.aggregatable).toEqual(true); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 808afc3449c2a..4a22508f7fef3 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -18,6 +18,7 @@ */ import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; +import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; @@ -129,7 +130,8 @@ export class IndexPatternField implements IFieldType { } public get visualizable() { - return this.aggregatable; + const notVisualizableFieldTypes: string[] = [KBN_FIELD_TYPES.UNKNOWN, KBN_FIELD_TYPES.CONFLICT]; + return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type); } public toJSON() { diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index ed84aceb60e5a..dc4da2456b47b 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -2,6 +2,7 @@ exports[`IndexPattern toSpec should match snapshot 1`] = ` Object { + "fieldFormats": Object {}, "fields": Object { "@tags": Object { "aggregatable": true, diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap index 752fdcf11991c..a3d19f311b765 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap @@ -2,6 +2,9 @@ exports[`IndexPatterns savedObjectToSpec 1`] = ` Object { + "fieldFormats": Object { + "field": Object {}, + }, "fields": Object {}, "id": "id", "intervalName": undefined, 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 6e11bc8f1d508..9085ae07bbe3e 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 @@ -44,7 +44,6 @@ function create(id: string) { return new IndexPattern({ spec: { id, type, version, timeFieldName, fields, title }, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], @@ -183,6 +182,20 @@ describe('IndexPattern', () => { }); }); + describe('setFieldFormat and deleteFieldFormaat', () => { + test('should persist changes', () => { + const formatter = { + toJSON: () => ({ id: 'bytes' }), + } as FieldFormat; + indexPattern.getFormatterForField = () => formatter; + indexPattern.setFieldFormat('bytes', { id: 'bytes' }); + expect(indexPattern.toSpec().fieldFormats).toEqual({ bytes: { id: 'bytes' } }); + + indexPattern.deleteFieldFormat('bytes'); + expect(indexPattern.toSpec().fieldFormats).toEqual({}); + }); + }); + describe('toSpec', () => { test('should match snapshot', () => { const formatter = { @@ -200,7 +213,6 @@ describe('IndexPattern', () => { const spec = indexPattern.toSpec(); const restoredPattern = new IndexPattern({ spec, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], @@ -209,7 +221,6 @@ describe('IndexPattern', () => { expect(restoredPattern.title).toEqual(indexPattern.title); expect(restoredPattern.timeFieldName).toEqual(indexPattern.timeFieldName); expect(restoredPattern.fields.length).toEqual(indexPattern.fields.length); - expect(restoredPattern.fieldFormatMap.bytes instanceof MockFieldFormatter).toEqual(true); }); }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 5fc6344c935d5..a0f27078543a9 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,16 +18,9 @@ */ import _, { each, reject } from 'lodash'; -import { SavedObjectsClientCommon } from '../..'; import { DuplicateField } from '../../../../kibana_utils/common'; -import { - ES_FIELD_TYPES, - KBN_FIELD_TYPES, - IIndexPattern, - FieldFormatNotFoundError, - IFieldType, -} from '../../../common'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; import { IndexPatternField, IIndexPatternFieldList, fieldList } from '../fields'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; @@ -37,10 +30,9 @@ import { SerializedFieldFormat } from '../../../../expressions/common'; interface IndexPatternDeps { spec?: IndexPatternSpec; - savedObjectsClient: SavedObjectsClientCommon; fieldFormats: FieldFormatsStartCommon; - shortDotsEnable: boolean; - metaFields: string[]; + shortDotsEnable?: boolean; + metaFields?: string[]; } interface SavedObjectBody { @@ -102,7 +94,7 @@ export class IndexPattern implements IIndexPattern { // set values this.id = spec.id; - const fieldFormatMap = this.fieldSpecsToFieldFormatMap(spec.fields); + this.fieldFormatMap = spec.fieldFormats || {}; this.version = spec.version; @@ -113,12 +105,16 @@ export class IndexPattern implements IIndexPattern { this.fields.replaceAll(Object.values(spec.fields || {})); this.type = spec.type; this.typeMeta = spec.typeMeta; - - this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { - return this.deserializeFieldFormatMap(mapping); - }); } + setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { + this.fieldFormatMap[fieldName] = format; + }; + + deleteFieldFormat = (fieldName: string) => { + delete this.fieldFormatMap[fieldName]; + }; + /** * Get last saved saved object fields */ @@ -131,34 +127,6 @@ export class IndexPattern implements IIndexPattern { this.originalSavedObjectBody = this.getAsSavedObjectBody(); }; - /** - * Converts field format spec to field format instance - * @param mapping - */ - private deserializeFieldFormatMap(mapping: SerializedFieldFormat>) { - try { - return this.fieldFormats.getInstance(mapping.id as string, mapping.params); - } catch (err) { - if (err instanceof FieldFormatNotFoundError) { - return undefined; - } else { - throw err; - } - } - } - - /** - * Extracts FieldFormatMap from FieldSpec map - * @param fldList FieldSpec map - */ - private fieldSpecsToFieldFormatMap = (fldList: IndexPatternSpec['fields'] = {}) => - Object.values(fldList).reduce>((col, fieldSpec) => { - if (fieldSpec.format) { - col[fieldSpec.name] = { ...fieldSpec.format }; - } - return col; - }, {}); - getComputedFields() { const scriptFields: any = {}; if (!this.fields) { @@ -211,6 +179,7 @@ export class IndexPattern implements IIndexPattern { fields: this.fields.toSpec({ getFormatterForField: this.getFormatterForField.bind(this) }), typeMeta: this.typeMeta, type: this.type, + fieldFormats: this.fieldFormatMap, }; } @@ -299,17 +268,9 @@ export class IndexPattern implements IIndexPattern { * Returns index pattern as saved object body for saving */ getAsSavedObjectBody() { - const serializeFieldFormatMap = ( - flat: any, - format: FieldFormat | undefined, - field: string | undefined - ) => { - if (format && field) { - flat[field] = format; - } - }; - const serialized = _.transform(this.fieldFormatMap, serializeFieldFormatMap); - const fieldFormatMap = _.isEmpty(serialized) ? undefined : JSON.stringify(serialized); + const fieldFormatMap = _.isEmpty(this.fieldFormatMap) + ? undefined + : JSON.stringify(this.fieldFormatMap); return { title: this.title, @@ -330,12 +291,25 @@ export class IndexPattern implements IIndexPattern { getFormatterForField( field: IndexPatternField | IndexPatternField['spec'] | IFieldType ): FieldFormat { - return ( - this.fieldFormatMap[field.name] || - this.fieldFormats.getDefaultInstance( + const formatSpec = this.fieldFormatMap[field.name]; + if (formatSpec) { + return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params); + } else { + return this.fieldFormats.getDefaultInstance( field.type as KBN_FIELD_TYPES, field.esTypes as ES_FIELD_TYPES[] - ) - ); + ); + } + } + + /** + * Get formatter for a given field name. Return undefined if none exists + * @param field + */ + getFormatterForFieldNoDefault(fieldname: string) { + const formatSpec = this.fieldFormatMap[fieldname]; + if (formatSpec) { + return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params); + } } } 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 9a86541376cd8..fd3d7a1d138fd 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 @@ -35,7 +35,6 @@ import { IndexPatternSpec, IndexPatternAttributes, FieldSpec, - FieldFormatMap, IndexPatternFieldMap, } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; @@ -296,20 +295,6 @@ export class IndexPatternsService { return fields; }; - /** - * Applies a set of formats to a set of fields - * @param fieldSpecs - * @param fieldFormatMap - */ - private addFormatsToFields = (fieldSpecs: FieldSpec[], fieldFormatMap: FieldFormatMap) => { - Object.entries(fieldFormatMap).forEach(([fieldName, value]) => { - const field = fieldSpecs.find((fld: FieldSpec) => fld.name === fieldName); - if (field) { - field.format = value; - } - }); - }; - /** * Converts field array to map * @param fields @@ -346,7 +331,6 @@ export class IndexPatternsService { const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {}; const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : []; - this.addFormatsToFields(parsedFields, parsedFieldFormatMap); return { id, version, @@ -357,6 +341,7 @@ export class IndexPatternsService { fields: this.fieldArrayToMap(parsedFields), typeMeta: parsedTypeMeta, type, + fieldFormats: parsedFieldFormatMap, }; }; @@ -382,9 +367,6 @@ export class IndexPatternsService { const spec = this.savedObjectToSpec(savedObject); const { title, type, typeMeta } = spec; - const parsedFieldFormats: FieldFormatMap = savedObject.attributes.fieldFormatMap - ? JSON.parse(savedObject.attributes.fieldFormatMap) - : {}; const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields); let isSaveRequired = isFieldRefreshRequired; @@ -415,12 +397,9 @@ export class IndexPatternsService { } } - Object.entries(parsedFieldFormats).forEach(([fieldName, value]) => { - const field = spec.fields?.[fieldName]; - if (field) { - field.format = value; - } - }); + spec.fieldFormats = savedObject.attributes.fieldFormatMap + ? JSON.parse(savedObject.attributes.fieldFormatMap) + : {}; const indexPattern = await this.create(spec, true); indexPatternCache.set(id, indexPattern); @@ -456,7 +435,6 @@ export class IndexPatternsService { const indexPattern = new IndexPattern({ spec, - savedObjectsClient: this.savedObjectsClient, fieldFormats: this.fieldFormats, shortDotsEnable, metaFields, diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index cb0c3aa0de38e..3387bc3b3c19e 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -172,6 +172,7 @@ export interface IndexPatternSpec { fields?: IndexPatternFieldMap; typeMeta?: TypeMeta; type?: string; + fieldFormats?: Record; } export interface SourceFilter { diff --git a/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts b/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts index 6a2d6edd04692..dd1a9a7f689a9 100644 --- a/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts +++ b/src/plugins/data/common/kbn_field_types/kbn_field_types.test.ts @@ -66,6 +66,7 @@ describe('utils/kbn_field_types', () => { test('returns the kbnFieldType name that matches the esType', () => { expect(castEsToKbnFieldTypeName(ES_FIELD_TYPES.KEYWORD)).toBe('string'); expect(castEsToKbnFieldTypeName(ES_FIELD_TYPES.FLOAT)).toBe('number'); + expect(castEsToKbnFieldTypeName(ES_FIELD_TYPES.UNSIGNED_LONG)).toBe('number'); }); test('returns unknown for unknown es types', () => { diff --git a/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts b/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts index b93ebcbbca9c8..373cdfda30607 100644 --- a/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts +++ b/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts @@ -48,6 +48,7 @@ export const createKbnFieldTypes = (): KbnFieldType[] => [ ES_FIELD_TYPES.DOUBLE, ES_FIELD_TYPES.INTEGER, ES_FIELD_TYPES.LONG, + ES_FIELD_TYPES.UNSIGNED_LONG, ES_FIELD_TYPES.SHORT, ES_FIELD_TYPES.BYTE, ES_FIELD_TYPES.TOKEN_COUNT, diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts index acd7a36b01fb3..ba9fd3e70b315 100644 --- a/src/plugins/data/common/kbn_field_types/types.ts +++ b/src/plugins/data/common/kbn_field_types/types.ts @@ -52,6 +52,7 @@ export enum ES_FIELD_TYPES { INTEGER = 'integer', LONG = 'long', SHORT = 'short', + UNSIGNED_LONG = 'unsigned_long', NESTED = 'nested', BYTE = 'byte', diff --git a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap new file mode 100644 index 0000000000000..d5ddaa31b8ac3 --- /dev/null +++ b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tabifyDocs converts fields by default 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested.field", + "meta": Object { + "field": "nested.field", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs converts source if option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "sourceTest", + "meta": Object { + "field": "sourceTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "sourceTest", + }, + ], + "rows": Array [ + Object { + "sourceTest": 123, + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs skips nested fields if option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested", + "meta": Object { + "field": "nested", + "index": "test-index", + "params": undefined, + "type": "object", + }, + "name": "nested", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested": Array [ + Object { + "field": 123, + }, + ], + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs works without provided index pattern 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested.field", + "meta": Object { + "field": "nested.field", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts index 90ac3f2fb730b..9e6657f5e8d83 100644 --- a/src/plugins/data/common/search/tabify/index.ts +++ b/src/plugins/data/common/search/tabify/index.ts @@ -17,6 +17,26 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; +import { SearchSource } from '../search_source'; +import { tabifyAggResponse } from './tabify'; +import { tabifyDocs, TabifyDocsOptions } from './tabify_docs'; +import { TabbedResponseWriterOptions } from './types'; + +export const tabify = ( + searchSource: SearchSource, + esResponse: SearchResponse, + opts: Partial | TabifyDocsOptions +) => { + return !esResponse.aggregations + ? tabifyDocs(esResponse, searchSource.getField('index'), opts as TabifyDocsOptions) + : tabifyAggResponse( + searchSource.getField('aggs'), + esResponse, + opts as Partial + ); +}; + export { tabifyAggResponse } from './tabify'; export { tabifyGetColumns } from './get_columns'; diff --git a/src/plugins/data/common/search/tabify/tabify_docs.test.ts b/src/plugins/data/common/search/tabify/tabify_docs.test.ts new file mode 100644 index 0000000000000..a1218928561c6 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.test.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 { tabifyDocs } from './tabify_docs'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { SearchResponse } from 'elasticsearch'; + +describe('tabifyDocs', () => { + const fieldFormats = { + getInstance: (id: string) => ({ toJSON: () => ({ id }) }), + getDefaultInstance: (id: string) => ({ toJSON: () => ({ id }) }), + }; + + const index = new IndexPattern({ + spec: { + id: 'test-index', + fields: { + sourceTest: { name: 'sourceTest', type: 'number', searchable: true, aggregatable: true }, + fieldTest: { name: 'fieldTest', type: 'number', searchable: true, aggregatable: true }, + 'nested.field': { + name: 'nested.field', + type: 'number', + searchable: true, + aggregatable: true, + }, + }, + }, + fieldFormats: fieldFormats as any, + }); + + const response = { + hits: { + hits: [ + { + _source: { sourceTest: 123 }, + fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, + }, + ], + }, + } as SearchResponse; + + it('converts fields by default', () => { + const table = tabifyDocs(response, index); + expect(table).toMatchSnapshot(); + }); + + it('converts source if option is set', () => { + const table = tabifyDocs(response, index, { source: true }); + expect(table).toMatchSnapshot(); + }); + + it('skips nested fields if option is set', () => { + const table = tabifyDocs(response, index, { shallow: true }); + expect(table).toMatchSnapshot(); + }); + + it('works without provided index pattern', () => { + const table = tabifyDocs(response); + expect(table).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data/common/search/tabify/tabify_docs.ts b/src/plugins/data/common/search/tabify/tabify_docs.ts new file mode 100644 index 0000000000000..78ebee9e65717 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { isPlainObject } from 'lodash'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { Datatable, DatatableColumn, DatatableColumnType } from '../../../../expressions/common'; + +export function flattenHit( + hit: Record, + indexPattern?: IndexPattern, + shallow: boolean = false +) { + const flat = {} as Record; + + function flatten(obj: Record, keyPrefix: string = '') { + for (const [k, val] of Object.entries(obj)) { + const key = keyPrefix + k; + + const field = indexPattern?.fields.getByName(key); + + if (!shallow) { + const isNestedField = field?.type === 'nested'; + if (Array.isArray(val) && !isNestedField) { + val.forEach((v) => isPlainObject(v) && flatten(v, key + '.')); + continue; + } + } else if (flat[key] !== undefined) { + continue; + } + + const hasValidMapping = field?.type !== 'conflict'; + const isValue = !isPlainObject(val); + + if (hasValidMapping || isValue) { + if (!flat[key]) { + flat[key] = val; + } else if (Array.isArray(flat[key])) { + flat[key].push(val); + } else { + flat[key] = [flat[key], val]; + } + continue; + } + + flatten(val, key + '.'); + } + } + + flatten(hit); + return flat; +} + +export interface TabifyDocsOptions { + shallow?: boolean; + source?: boolean; +} + +export const tabifyDocs = ( + esResponse: SearchResponse, + index?: IndexPattern, + params: TabifyDocsOptions = {} +): Datatable => { + const columns: DatatableColumn[] = []; + + const rows = esResponse.hits.hits + .map((hit) => { + const toConvert = params.source ? hit._source : hit.fields; + const flat = flattenHit(toConvert, index, params.shallow); + for (const [key, value] of Object.entries(flat)) { + const field = index?.fields.getByName(key); + const fieldName = field?.name || key; + if (!columns.find((c) => c.id === fieldName)) { + const fieldType = (field?.type as DatatableColumnType) || typeof value; + const formatter = field && index?.getFormatterForField(field); + columns.push({ + id: fieldName, + name: fieldName, + meta: { + type: fieldType, + field: fieldName, + index: index?.id, + params: formatter ? formatter.toJSON() : undefined, + }, + }); + } + } + return flat; + }) + .filter((hit) => hit); + + return { + type: 'datatable', + columns, + rows, + }; +}; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d280b6f1faf7d..d2439e3f1573c 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -514,7 +514,9 @@ export enum ES_FIELD_TYPES { // (undocumented) TOKEN_COUNT = "token_count", // (undocumented) - _TYPE = "_type" + _TYPE = "_type", + // (undocumented) + UNSIGNED_LONG = "unsigned_long" } // Warning: (ae-missing-release-tag) "ES_SEARCH_STRATEGY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1063,6 +1065,8 @@ export class IndexPattern implements IIndexPattern { constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) + deleteFieldFormat: (fieldName: string) => void; + // (undocumented) fieldFormatMap: Record; // (undocumented) fields: IIndexPatternFieldList & { @@ -1108,6 +1112,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; + getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; // (undocumented) getNonScriptedFields(): IndexPatternField[]; getOriginalSavedObjectBody: () => { @@ -1139,6 +1144,8 @@ export class IndexPattern implements IIndexPattern { metaFields: string[]; removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; + // (undocumented) + setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1287,6 +1294,8 @@ export type IndexPatternSelectProps = Required, 'isLo // // @public (undocumented) export interface IndexPatternSpec { + // (undocumented) + fieldFormats?: Record; // (undocumented) fields?: IndexPatternFieldMap; // (undocumented) @@ -2274,7 +2283,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index 57c636a9e3c69..e75b8761984ec 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { getFieldCapabilities, resolveTimePattern, createNoMatchingIndicesError } from './lib'; @@ -37,10 +37,12 @@ interface FieldSubType { } export class IndexPatternsFetcher { - private _callDataCluster: LegacyAPICaller; + private elasticsearchClient: ElasticsearchClient; + private allowNoIndices: boolean; - constructor(callDataCluster: LegacyAPICaller) { - this._callDataCluster = callDataCluster; + constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) { + this.elasticsearchClient = elasticsearchClient; + this.allowNoIndices = allowNoIndices; } /** @@ -55,10 +57,12 @@ export class IndexPatternsFetcher { async getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; - fieldCapsOptions?: { allowNoIndices: boolean }; + fieldCapsOptions?: { allow_no_indices: boolean }; }): Promise { const { pattern, metaFields, fieldCapsOptions } = options; - return await getFieldCapabilities(this._callDataCluster, pattern, metaFields, fieldCapsOptions); + return await getFieldCapabilities(this.elasticsearchClient, pattern, metaFields, { + allow_no_indices: fieldCapsOptions ? fieldCapsOptions.allow_no_indices : this.allowNoIndices, + }); } /** @@ -78,11 +82,11 @@ export class IndexPatternsFetcher { interval: string; }) { const { pattern, lookBack, metaFields } = options; - const { matches } = await resolveTimePattern(this._callDataCluster, pattern); + const { matches } = await resolveTimePattern(this.elasticsearchClient, pattern); const indices = matches.slice(0, lookBack); if (indices.length === 0) { throw createNoMatchingIndicesError(pattern); } - return await getFieldCapabilities(this._callDataCluster, indices, metaFields); + return await getFieldCapabilities(this.elasticsearchClient, indices, metaFields); } } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js index 8078ea32187b3..fad20a8f0be06 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js @@ -32,36 +32,60 @@ describe('server/index_patterns/service/lib/es_api', () => { afterEach(() => sandbox.restore()); it('calls indices.getAlias() via callCluster', async () => { - const callCluster = sinon.stub(); + const getAlias = sinon.stub(); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; + await callIndexAliasApi(callCluster); - sinon.assert.calledOnce(callCluster); - sinon.assert.calledWith(callCluster, 'indices.getAlias'); + sinon.assert.calledOnce(getAlias); }); it('passes indices directly to es api', async () => { const football = {}; - const callCluster = sinon.stub(); + const getAlias = sinon.stub(); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; await callIndexAliasApi(callCluster, football); - sinon.assert.calledOnce(callCluster); - expect(callCluster.args[0][1].index).toBe(football); + sinon.assert.calledOnce(getAlias); + expect(getAlias.args[0][0].index).toBe(football); }); it('returns the es response directly', async () => { const football = {}; - const callCluster = sinon.stub().returns(football); + const getAlias = sinon.stub().returns(football); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; const resp = await callIndexAliasApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(getAlias); expect(resp).toBe(football); }); it('sets ignoreUnavailable and allowNoIndices params', async () => { - const callCluster = sinon.stub(); + const getAlias = sinon.stub(); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; await callIndexAliasApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(getAlias); - const passedOpts = callCluster.args[0][1]; - expect(passedOpts).toHaveProperty('ignoreUnavailable', true); - expect(passedOpts).toHaveProperty('allowNoIndices', false); + const passedOpts = getAlias.args[0][0]; + expect(passedOpts).toHaveProperty('ignore_unavailable', true); + expect(passedOpts).toHaveProperty('allow_no_indices', false); }); it('handles errors with convertEsError()', async () => { @@ -70,9 +94,15 @@ describe('server/index_patterns/service/lib/es_api', () => { const convertedError = new Error('convertedError'); sandbox.stub(convertEsErrorNS, 'convertEsError').throws(convertedError); - const callCluster = sinon.spy(async () => { + const getAlias = sinon.stub(async () => { throw esError; }); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; try { await callIndexAliasApi(callCluster, indices); throw new Error('expected callIndexAliasApi() to throw'); @@ -91,37 +121,60 @@ describe('server/index_patterns/service/lib/es_api', () => { afterEach(() => sandbox.restore()); it('calls fieldCaps() via callCluster', async () => { - const callCluster = sinon.stub(); + const fieldCaps = sinon.stub(); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; await callFieldCapsApi(callCluster); - sinon.assert.calledOnce(callCluster); - sinon.assert.calledWith(callCluster, 'fieldCaps'); + sinon.assert.calledOnce(fieldCaps); }); it('passes indices directly to es api', async () => { const football = {}; - const callCluster = sinon.stub(); + const fieldCaps = sinon.stub(); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; await callFieldCapsApi(callCluster, football); - sinon.assert.calledOnce(callCluster); - expect(callCluster.args[0][1].index).toBe(football); + sinon.assert.calledOnce(fieldCaps); + expect(fieldCaps.args[0][0].index).toBe(football); }); it('returns the es response directly', async () => { const football = {}; - const callCluster = sinon.stub().returns(football); + const fieldCaps = sinon.stub().returns(football); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; const resp = await callFieldCapsApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(fieldCaps); expect(resp).toBe(football); }); it('sets ignoreUnavailable, allowNoIndices, and fields params', async () => { - const callCluster = sinon.stub(); + const fieldCaps = sinon.stub(); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; await callFieldCapsApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(fieldCaps); - const passedOpts = callCluster.args[0][1]; + const passedOpts = fieldCaps.args[0][0]; expect(passedOpts).toHaveProperty('fields', '*'); - expect(passedOpts).toHaveProperty('ignoreUnavailable', true); - expect(passedOpts).toHaveProperty('allowNoIndices', false); + expect(passedOpts).toHaveProperty('ignore_unavailable', true); + expect(passedOpts).toHaveProperty('allow_no_indices', false); }); it('handles errors with convertEsError()', async () => { @@ -130,9 +183,15 @@ describe('server/index_patterns/service/lib/es_api', () => { const convertedError = new Error('convertedError'); sandbox.stub(convertEsErrorNS, 'convertEsError').throws(convertedError); - const callCluster = sinon.spy(async () => { + const fieldCaps = sinon.spy(async () => { throw esError; }); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; try { await callFieldCapsApi(callCluster, indices); throw new Error('expected callFieldCapsApi() to throw'); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts index 27ce14f9a3597..7969324943a9f 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { convertEsError } from './errors'; import { FieldCapsResponse } from './field_capabilities'; @@ -46,15 +46,15 @@ export interface IndexAliasResponse { * @return {Promise} */ export async function callIndexAliasApi( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, indices: string[] | string -): Promise { +) { try { - return (await callCluster('indices.getAlias', { + return await callCluster.indices.getAlias({ index: indices, - ignoreUnavailable: true, - allowNoIndices: false, - })) as Promise; + ignore_unavailable: true, + allow_no_indices: false, + }); } catch (error) { throw convertEsError(indices, error); } @@ -73,17 +73,17 @@ export async function callIndexAliasApi( * @return {Promise} */ export async function callFieldCapsApi( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, indices: string[] | string, - fieldCapsOptions: { allowNoIndices: boolean } = { allowNoIndices: false } + fieldCapsOptions: { allow_no_indices: boolean } = { allow_no_indices: false } ) { try { - return (await callCluster('fieldCaps', { + return await callCluster.fieldCaps({ index: indices, fields: '*', - ignoreUnavailable: true, + ignore_unavailable: true, ...fieldCapsOptions, - })) as FieldCapsResponse; + }); } catch (error) { throw convertEsError(indices, error); } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js index 0e5757b7b782b..2d860dc8b1843 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js @@ -48,9 +48,11 @@ describe('index_patterns/field_capabilities/field_capabilities', () => { }; const stubDeps = (options = {}) => { - const { esResponse = {}, fieldsFromFieldCaps = [], mergeOverrides = identity } = options; + const { esResponse = [], fieldsFromFieldCaps = [], mergeOverrides = identity } = options; - sandbox.stub(callFieldCapsApiNS, 'callFieldCapsApi').callsFake(async () => esResponse); + sandbox + .stub(callFieldCapsApiNS, 'callFieldCapsApi') + .callsFake(async () => ({ body: esResponse })); sandbox.stub(readFieldCapsResponseNS, 'readFieldCapsResponse').returns(fieldsFromFieldCaps); sandbox.stub(mergeOverridesNS, 'mergeOverrides').callsFake(mergeOverrides); }; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index 62e77e0adad66..b9e3e8aae0899 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -19,9 +19,9 @@ import { defaults, keyBy, sortBy } from 'lodash'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { callFieldCapsApi } from '../es_api'; -import { FieldCapsResponse, readFieldCapsResponse } from './field_caps_response'; +import { readFieldCapsResponse } from './field_caps_response'; import { mergeOverrides } from './overrides'; import { FieldDescriptor } from '../../index_patterns_fetcher'; @@ -36,17 +36,13 @@ import { FieldDescriptor } from '../../index_patterns_fetcher'; * @return {Promise>} */ export async function getFieldCapabilities( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, indices: string | string[] = [], metaFields: string[] = [], - fieldCapsOptions?: { allowNoIndices: boolean } + fieldCapsOptions?: { allow_no_indices: boolean } ) { - const esFieldCaps: FieldCapsResponse = await callFieldCapsApi( - callCluster, - indices, - fieldCapsOptions - ); - const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name'); + const esFieldCaps = await callFieldCapsApi(callCluster, indices, fieldCapsOptions); + const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps.body), 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) .filter((name) => !name.startsWith('_')) diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js index 660e9ec30db6a..87f222aaad89d 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js @@ -32,6 +32,11 @@ const TIME_PATTERN = '[logs-]dddd-YYYY.w'; describe('server/index_patterns/service/lib/resolve_time_pattern', () => { let sandbox; + const esClientMock = { + indices: { + getAlias: () => ({}), + }, + }; beforeEach(() => (sandbox = sinon.createSandbox())); afterEach(() => sandbox.restore()); @@ -39,7 +44,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { describe('pre request', () => { it('uses callIndexAliasApi() fn', async () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({}); - await resolveTimePattern(noop, TIME_PATTERN); + await resolveTimePattern(esClientMock, TIME_PATTERN); sinon.assert.calledOnce(callIndexAliasApi); }); @@ -49,7 +54,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard); - await resolveTimePattern(noop, timePattern); + await resolveTimePattern(esClientMock, timePattern); sinon.assert.calledOnce(timePatternToWildcard); expect(timePatternToWildcard.firstCall.args).toEqual([timePattern]); }); @@ -61,7 +66,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({}); sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard); - await resolveTimePattern(noop, timePattern); + await resolveTimePattern(esClientMock, timePattern); sinon.assert.calledOnce(callIndexAliasApi); expect(callIndexAliasApi.firstCall.args[1]).toBe(wildcard); }); @@ -70,13 +75,15 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { describe('read response', () => { it('returns all aliases names in result.all, ordered by time desc', async () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({ - 'logs-2016.2': {}, - 'logs-Saturday-2017.1': {}, - 'logs-2016.1': {}, - 'logs-Sunday-2017.1': {}, - 'logs-2015': {}, - 'logs-2016.3': {}, - 'logs-Friday-2017.1': {}, + body: { + 'logs-2016.2': {}, + 'logs-Saturday-2017.1': {}, + 'logs-2016.1': {}, + 'logs-Sunday-2017.1': {}, + 'logs-2015': {}, + 'logs-2016.3': {}, + 'logs-Friday-2017.1': {}, + }, }); const resp = await resolveTimePattern(noop, TIME_PATTERN); @@ -94,13 +101,15 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { it('returns all indices matching the time pattern in matches, ordered by time desc', async () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({ - 'logs-2016.2': {}, - 'logs-Saturday-2017.1': {}, - 'logs-2016.1': {}, - 'logs-Sunday-2017.1': {}, - 'logs-2015': {}, - 'logs-2016.3': {}, - 'logs-Friday-2017.1': {}, + body: { + 'logs-2016.2': {}, + 'logs-Saturday-2017.1': {}, + 'logs-2016.1': {}, + 'logs-Sunday-2017.1': {}, + 'logs-2015': {}, + 'logs-2016.3': {}, + 'logs-Friday-2017.1': {}, + }, }); const resp = await resolveTimePattern(noop, TIME_PATTERN); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts index 2e408d7569be5..95ec06dd9c6e6 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts @@ -20,7 +20,7 @@ import { chain } from 'lodash'; import moment from 'moment'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { timePatternToWildcard } from './time_pattern_to_wildcard'; import { callIndexAliasApi, IndicesAliasResponse } from './es_api'; @@ -36,10 +36,10 @@ import { callIndexAliasApi, IndicesAliasResponse } from './es_api'; * and the indices that actually match the time * pattern (matches); */ -export async function resolveTimePattern(callCluster: LegacyAPICaller, timePattern: string) { +export async function resolveTimePattern(callCluster: ElasticsearchClient, timePattern: string) { const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern)); - const allIndexDetails = chain(aliases) + const allIndexDetails = chain(aliases.body) .reduce( (acc: string[], index: any, indexName: string) => acc.concat(indexName, Object.keys(index.aliases || {})), diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 428e7fef6deea..041eb235d01e0 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -46,8 +46,8 @@ export function registerRoutes(http: HttpServiceSetup) { }, }, async (context, request, response) => { - const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; - const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { asCurrentUser } = context.core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, meta_fields: metaFields } = request.query; let parsedFields: string[] = []; @@ -105,8 +105,8 @@ export function registerRoutes(http: HttpServiceSetup) { }, }, async (context: RequestHandlerContext, request: any, response: any) => { - const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; - const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { asCurrentUser } = context.core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, interval, look_back: lookBack, meta_fields: metaFields } = request.query; let parsedFields: string[] = []; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 0ed296a1d0662..a0cac76cc2cda 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -211,7 +211,9 @@ export enum ES_FIELD_TYPES { // (undocumented) TOKEN_COUNT = "token_count", // (undocumented) - _TYPE = "_type" + _TYPE = "_type", + // (undocumented) + UNSIGNED_LONG = "unsigned_long" } // Warning: (ae-missing-release-tag) "ES_SEARCH_STRATEGY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -513,6 +515,8 @@ export class IndexPattern implements IIndexPattern { constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) + deleteFieldFormat: (fieldName: string) => void; + // (undocumented) fieldFormatMap: Record; // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts // @@ -560,6 +564,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; + getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; // Warning: (ae-forgotten-export) The symbol "IndexPatternField" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -593,6 +598,10 @@ export class IndexPattern implements IIndexPattern { metaFields: string[]; removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; + // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts + // + // (undocumented) + setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -649,7 +658,7 @@ export const indexPatterns: { // // @public (undocumented) export class IndexPatternsFetcher { - constructor(callDataCluster: LegacyAPICaller); + constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean); getFieldsForTimePattern(options: { pattern: string; metaFields: string[]; @@ -660,7 +669,7 @@ export class IndexPatternsFetcher { pattern: string | string[]; metaFields?: string[]; fieldCapsOptions?: { - allowNoIndices: boolean; + allow_no_indices: boolean; }; }): Promise; } @@ -1104,8 +1113,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:56:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover/public/application/angular/context/query/actions.js index 32fc2873d7f2a..d5c72d34006e2 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.js +++ b/src/plugins/discover/public/application/angular/context/query/actions.js @@ -25,7 +25,7 @@ import { getServices } from '../../../../kibana_services'; import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; -import { FAILURE_REASONS, LOADING_STATUS } from './constants'; +import { FAILURE_REASONS, LOADING_STATUS } from './index'; import { MarkdownSimple } from '../../../../../../kibana_react/public'; export function QueryActionsProvider(Promise) { diff --git a/src/plugins/discover/public/application/angular/context/query/index.js b/src/plugins/discover/public/application/angular/context/query/index.js index f9b1a35e32fea..59d1f165d19d4 100644 --- a/src/plugins/discover/public/application/angular/context/query/index.js +++ b/src/plugins/discover/public/application/angular/context/query/index.js @@ -18,5 +18,5 @@ */ export { QueryActionsProvider } from './actions'; -export { FAILURE_REASONS, LOADING_STATUS } from './constants'; +export { FAILURE_REASONS, LOADING_STATUS } from '../../../components/context_app/constants'; export { createInitialLoadingStatusState } from './state'; diff --git a/src/plugins/discover/public/application/angular/context/query/state.js b/src/plugins/discover/public/application/angular/context/query/state.js index 06fd0680d347f..142b5746249bf 100644 --- a/src/plugins/discover/public/application/angular/context/query/state.js +++ b/src/plugins/discover/public/application/angular/context/query/state.js @@ -17,7 +17,7 @@ * under the License. */ -import { LOADING_STATUS } from './constants'; +import { LOADING_STATUS } from './index'; export function createInitialLoadingStatusState() { return { diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 6adcaeeae94f5..d609a497c4ba1 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -12,8 +12,8 @@ - @@ -35,39 +35,17 @@ type="'predecessors'" > - - -
-
-
- -
-
- -
-
+ , -
-
-
-
- -
-
-
-

- Refine your query -

-

- The search bar at the top uses Elasticsearch’s support for Lucene - - Query String syntax - - . Here are some examples of how you can search for web server logs that have been parsed into a few fields. -

-
-
-
-
-
- - Find requests that contain the number 200, in any field - -
-
-
- - - 200 - - -
-
-
- - Find 200 in the status field - -
-
-
- - - status:200 - - -
-
-
- - Find all status codes between 400-499 - -
-
-
- - - status:[400 TO 499] - - -
-
-
- - Find status codes 400-499 with the extension php - -
-
-
- - - status:[400 TO 499] AND extension:PHP - - -
-
-
- - Find status codes 400-499 with the extension php or html - -
-
-
- - - status:[400 TO 499] AND (extension:php OR extension:html) - - -
-
-
-
-
, -] -`; - -exports[`DiscoverNoResults props timeFieldName renders time range feedback 1`] = ` -Array [ -
, -
-
-
-
- -
-
-
-

- Expand your time range -

-

- One or more of the indices you’re looking at contains a date field. Your query may not match anything in the current time range, or there may not be any data at all in the currently selected time range. You can try changing the time range to one which contains data. -

-
-
-
, -] -`; diff --git a/src/plugins/discover/public/application/angular/directives/_index.scss b/src/plugins/discover/public/application/angular/directives/_index.scss index d4b365547b40c..dfacdf45c9d7b 100644 --- a/src/plugins/discover/public/application/angular/directives/_index.scss +++ b/src/plugins/discover/public/application/angular/directives/_index.scss @@ -1,2 +1 @@ -@import 'no_results'; @import 'histogram'; diff --git a/src/plugins/discover/public/application/angular/directives/index.ts b/src/plugins/discover/public/application/angular/directives/index.ts new file mode 100644 index 0000000000000..2e120995cce07 --- /dev/null +++ b/src/plugins/discover/public/application/angular/directives/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 { DiscoverUninitialized } from './uninitialized'; +export { DiscoverHistogram } from './histogram'; diff --git a/src/plugins/discover/public/application/angular/directives/no_results.js b/src/plugins/discover/public/application/angular/directives/no_results.js deleted file mode 100644 index d8a39d9178e93..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/no_results.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Component, Fragment } from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import PropTypes from 'prop-types'; - -import { - EuiCallOut, - EuiCode, - EuiDescriptionList, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { getServices } from '../../../kibana_services'; - -// eslint-disable-next-line react/prefer-stateless-function -export class DiscoverNoResults extends Component { - static propTypes = { - timeFieldName: PropTypes.string, - queryLanguage: PropTypes.string, - }; - - render() { - const { timeFieldName, queryLanguage } = this.props; - - let timeFieldMessage; - - if (timeFieldName) { - timeFieldMessage = ( - - - - -

- -

- -

- -

-
-
- ); - } - - let luceneQueryMessage; - - if (queryLanguage === 'lucene') { - const searchExamples = [ - { - description: 200, - title: ( - - - - - - ), - }, - { - description: status:200, - title: ( - - - - - - ), - }, - { - description: status:[400 TO 499], - title: ( - - - - - - ), - }, - { - description: status:[400 TO 499] AND extension:PHP, - title: ( - - - - - - ), - }, - { - description: status:[400 TO 499] AND (extension:php OR extension:html), - title: ( - - - - - - ), - }, - ]; - - luceneQueryMessage = ( - - - - -

- -

- -

- - - - ), - }} - /> -

-
- - - - - - -
- ); - } - - return ( - - - - - - - - } - color="warning" - iconType="help" - data-test-subj="discoverNoResults" - /> - {timeFieldMessage} - {luceneQueryMessage} - - - - - ); - } -} diff --git a/src/plugins/discover/public/application/angular/directives/no_results.test.js b/src/plugins/discover/public/application/angular/directives/no_results.test.js deleted file mode 100644 index 60c50048a39ef..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/no_results.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { renderWithIntl } from 'test_utils/enzyme_helpers'; - -import { DiscoverNoResults } from './no_results'; - -jest.mock('../../../kibana_services', () => { - return { - getServices: () => ({ - docLinks: { - links: { - query: { - luceneQuerySyntax: 'documentation-link', - }, - }, - }, - }), - }; -}); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('DiscoverNoResults', () => { - describe('props', () => { - describe('timeFieldName', () => { - test('renders time range feedback', () => { - const component = renderWithIntl(); - - expect(component).toMatchSnapshot(); - }); - }); - - describe('queryLanguage', () => { - test('supports lucene and renders doc link', () => { - const component = renderWithIntl( - 'documentation-link'} /> - ); - - expect(component).toMatchSnapshot(); - }); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 612cedb7780bd..ebd086dd1e38a 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -87,6 +87,7 @@ const fetchStatuses = { UNINITIALIZED: 'uninitialized', LOADING: 'loading', COMPLETE: 'complete', + ERROR: 'error', }; const app = getAngularModule(); @@ -620,6 +621,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise config: config, fixedScroll: createFixedScroll($scope, $timeout), setHeaderActionMenu: getHeaderActionMenuMounter(), + data, }; const shouldSearchOnPageLoad = () => { @@ -685,7 +687,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise function pick(rows, oldRows, fetchStatus) { // initial state, pretend we're already loading if we're about to execute a search so // that the uninitilized message doesn't flash on screen - if (rows == null && oldRows == null && shouldSearchOnPageLoad()) { + if (!$scope.fetchError && rows == null && oldRows == null && shouldSearchOnPageLoad()) { return status.LOADING; } @@ -814,7 +816,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise if (error instanceof Error && error.name === 'AbortError') return; $scope.fetchStatus = fetchStatuses.NO_RESULTS; - $scope.rows = []; + $scope.fetchError = error; data.search.showError(error); }); diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index ad2b674af014c..f191fa2dc89e8 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -68,6 +68,7 @@ export function convertDirectiveToRenderFn( let rejected = false; const cleanupFnPromise = injectAngularElement(domNode, directive.template, props, getInjector); + cleanupFnPromise.catch(() => { rejected = true; render(
error
, domNode); @@ -91,10 +92,10 @@ export interface DocTableLegacyProps { rows: Array>; indexPattern: IIndexPattern; minimumVisibleRows: number; - onAddColumn: (column: string) => void; - onSort: (sort: string[][]) => void; - onMoveColumn: (columns: string, newIdx: number) => void; - onRemoveColumn: (column: string) => void; + onAddColumn?: (column: string) => void; + onSort?: (sort: string[][]) => void; + onMoveColumn?: (columns: string, newIdx: number) => void; + onRemoveColumn?: (column: string) => void; sort?: string[][]; } diff --git a/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap b/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap new file mode 100644 index 0000000000000..58305ee23cb21 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap @@ -0,0 +1,741 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContextAppLegacy test renders correctly 1`] = ` + + + + + +
+
+ +
+ +
+
+ + + + + +`; + +exports[`ContextAppLegacy test renders loading indicator 1`] = ` + + + + + +
+ +
+ +
+ + Loading... + +
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/discover/public/application/angular/context/query/constants.js b/src/plugins/discover/public/application/components/context_app/constants.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/query/constants.js rename to src/plugins/discover/public/application/components/context_app/constants.ts 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 new file mode 100644 index 0000000000000..16d8cd78004f9 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -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 React from 'react'; +import { ContextAppLegacy } from './context_app_legacy'; +import { IIndexPattern } from '../../../../../data/common/index_patterns'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('ContextAppLegacy test', () => { + const hit = { + _id: '123', + _index: 'test_index', + _score: null, + _version: 1, + _source: { + category: ["Men's Clothing"], + currency: 'EUR', + customer_first_name: 'Walker', + customer_full_name: 'Walker Texas Ranger', + customer_gender: 'MALE', + customer_last_name: 'Ranger', + }, + fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }], + sort: [1603114502000, 2092], + }; + const indexPattern = { + id: 'test_index_pattern', + } as IIndexPattern; + const defaultProps = { + columns: ['_source'], + filter: () => {}, + hits: [hit], + infiniteScroll: true, + sorting: ['order_date', 'desc'], + minimumVisibleRows: 5, + indexPattern, + status: 'loaded', + }; + + it('renders correctly', () => { + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + expect(component.find(DocTableLegacy).length).toBe(1); + const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); + expect(loadingIndicator.length).toBe(0); + }); + + it('renders loading indicator', () => { + const props = { ...defaultProps }; + props.status = 'loading'; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + expect(component.find('DocTableLegacy').length).toBe(0); + const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); + expect(loadingIndicator.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 new file mode 100644 index 0000000000000..ee8b2f590f71c --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.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 from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiText } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import { + DocTableLegacy, + DocTableLegacyProps, +} from '../../angular/doc_table/create_doc_table_react'; +import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { LOADING_STATUS } from './constants'; + +export interface ContextAppProps { + columns: string[]; + hits: Array>; + indexPattern: IIndexPattern; + filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + minimumVisibleRows: number; + sorting: string[]; + status: string; +} + +export function ContextAppLegacy(renderProps: ContextAppProps) { + const { hits, filter, sorting, status } = renderProps; + const props = ({ ...renderProps } as unknown) as DocTableLegacyProps; + props.rows = hits; + props.onFilter = filter; + props.sort = sorting.map((el) => [el]); + const isLoaded = status === LOADING_STATUS.LOADED; + const loadingFeedback = () => { + if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) { + return ( + + + + + + ); + } + return null; + }; + return ( + + + {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 new file mode 100644 index 0000000000000..af94c5537da28 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -0,0 +1,32 @@ +/* + * 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 { ContextAppLegacy } from './context_app_legacy'; + +export function createContextAppLegacy(reactDirective: any) { + return reactDirective(ContextAppLegacy, [ + ['filter', { watchDepth: 'reference' }], + ['hits', { watchDepth: 'reference' }], + ['indexPattern', { watchDepth: 'reference' }], + ['sorting', { watchDepth: 'reference' }], + ['columns', { watchDepth: 'collection' }], + ['infiniteScroll', { watchDepth: 'reference' }], + ['minimumVisibleRows', { watchDepth: 'reference' }], + ['status', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 139b2ca69d9e4..3ca421f809640 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -26,10 +26,8 @@ import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; import { DiscoverSidebar } from './sidebar'; import { getServices, IndexPattern } from '../../kibana_services'; -// @ts-ignore -import { DiscoverNoResults } from '../angular/directives/no_results'; -import { DiscoverUninitialized } from '../angular/directives/uninitialized'; -import { DiscoverHistogram } from '../angular/directives/histogram'; +import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; +import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; @@ -40,6 +38,7 @@ import { TimeRange, Query, IndexPatternAttributes, + DataPublicPluginStart, } from '../../../../data/public'; import { Chart } from '../angular/helpers/point_series'; import { AppState } from '../angular/discover_state'; @@ -53,6 +52,7 @@ export interface DiscoverLegacyProps { addColumn: (column: string) => void; fetch: () => void; fetchCounter: number; + fetchError: Error; fieldCounts: Record; histogramData: Chart; hits: number; @@ -73,6 +73,7 @@ export interface DiscoverLegacyProps { sampleSize: number; fixedScroll: (el: HTMLElement) => void; setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + data: DataPublicPluginStart; }; resetQuery: () => void; resultState: string; @@ -94,6 +95,7 @@ export function DiscoverLegacy({ fetch, fetchCounter, fieldCounts, + fetchError, histogramData, hits, indexPattern, @@ -208,6 +210,8 @@ export function DiscoverLegacy({ )} {resultState === 'uninitialized' && } 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 afb541253d994..d00a956b7c73d 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 @@ -10,7 +10,7 @@ exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` -
-
-
{ + return { + getServices: () => ({ + docLinks: { + links: { + query: { + luceneQuerySyntax: 'documentation-link', + }, + }, + }, + }), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +function mountAndFindSubjects(props: DiscoverNoResultsProps) { + const component = mountWithIntl(); + return { + mainMsg: findTestSubject(component, 'discoverNoResults').length > 0, + timeFieldMsg: findTestSubject(component, 'discoverNoResultsTimefilter').length > 0, + luceneMsg: findTestSubject(component, 'discoverNoResultsLucene').length > 0, + errorMsg: findTestSubject(component, 'discoverNoResultsError').length > 0, + }; +} + +describe('DiscoverNoResults', () => { + describe('props', () => { + describe('no props', () => { + test('renders default feedback', () => { + const result = mountAndFindSubjects({}); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": false, + "luceneMsg": false, + "mainMsg": true, + "timeFieldMsg": false, + } + `); + }); + }); + describe('timeFieldName', () => { + test('renders time range feedback', () => { + const result = mountAndFindSubjects({ + timeFieldName: 'awesome_time_field', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": false, + "luceneMsg": false, + "mainMsg": true, + "timeFieldMsg": true, + } + `); + }); + }); + + describe('queryLanguage', () => { + test('supports lucene and renders doc link', () => { + const result = mountAndFindSubjects({ queryLanguage: 'lucene' }); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": false, + "luceneMsg": true, + "mainMsg": true, + "timeFieldMsg": false, + } + `); + }); + }); + + describe('error message', () => { + test('renders error message', () => { + const error = new Error('Fatal error'); + const result = mountAndFindSubjects({ + timeFieldName: 'awesome_time_field', + error, + queryLanguage: 'lucene', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": true, + "luceneMsg": false, + "mainMsg": false, + "timeFieldMsg": false, + } + `); + }); + }); + }); +}); diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx new file mode 100644 index 0000000000000..fcc2912d16dd5 --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx @@ -0,0 +1,92 @@ +/* + * 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 } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { getServices } from '../../../kibana_services'; +import { DataPublicPluginStart } from '../../../../../data/public'; +import { getLuceneQueryMessage, getTimeFieldMessage } from './no_results_helper'; +import './_no_results.scss'; + +export interface DiscoverNoResultsProps { + timeFieldName?: string; + queryLanguage?: string; + error?: Error; + data?: DataPublicPluginStart; +} + +export function DiscoverNoResults({ + timeFieldName, + queryLanguage, + error, + data, +}: DiscoverNoResultsProps) { + const callOut = !error ? ( + + + } + color="warning" + iconType="help" + data-test-subj="discoverNoResults" + /> + {timeFieldName ? getTimeFieldMessage() : null} + {queryLanguage === 'lucene' + ? getLuceneQueryMessage(getServices().docLinks.links.query.luceneQuerySyntax) + : null} + + ) : ( + + + } + color="danger" + iconType="alert" + data-test-subj="discoverNoResultsError" + > + (data ? data.search.showError(error) : void 0)} + > + + + + + ); + + return ( + + + {callOut} + + ); +} diff --git a/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx b/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx new file mode 100644 index 0000000000000..fbc655e01bdf5 --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx @@ -0,0 +1,149 @@ +/* + * 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 } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode, EuiDescriptionList, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; + +export function getTimeFieldMessage() { + return ( + + + +

+ +

+

+ +

+
+
+ ); +} + +export function getLuceneQueryMessage(link: string) { + const searchExamples = [ + { + description: 200, + title: ( + + + + + + ), + }, + { + description: status:200, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499], + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND extension:PHP, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND (extension:php OR extension:html), + title: ( + + + + + + ), + }, + ]; + return ( + + + +

+ +

+

+ + + + ), + }} + /> +

+
+ + + +
+ ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx new file mode 100644 index 0000000000000..2cf626d182eeb --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.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 from 'react'; +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DiscoverFieldDetails } from './discover_field_details'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; + +const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() +); + +describe('discover sidebar field details', function () { + const defaultProps = { + indexPattern, + details: { buckets: [], error: '', exists: 1, total: true, columns: [] }, + onAddFilter: jest.fn(), + }; + + function mountComponent(field: IndexPatternField) { + const compProps = { ...defaultProps, field }; + return mountWithIntl(); + } + + it('should enable the visualize link for a number field', function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy(); + }); + + it('should disable the visualize link for an _id field', function () { + const conflictField = new IndexPatternField( + { + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'test' + ); + const comp = mountComponent(conflictField); + expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({}); + }); + + it('should disable the visualize link for an unknown field', function () { + const unknownField = new IndexPatternField( + { + name: 'test', + type: 'unknown', + esTypes: ['double'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'test' + ); + const comp = mountComponent(unknownField); + expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({}); + }); +}); diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 1ca0bb20e8723..55a75240909bf 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -36,6 +36,7 @@ import { createToolBarPagerButtonsDirective, createToolBarPagerTextDirective, } from './application/angular/doc_table/components/pager'; +import { createContextAppLegacy } from './application/components/context_app/context_app_legacy_directive'; import { createTableRowDirective } from './application/angular/doc_table/components/table_row'; import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; @@ -55,7 +56,6 @@ import { createContextErrorMessageDirective } from './application/components/con import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_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 @@ -190,5 +190,6 @@ function createDocTableModule() { .directive('kbnTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('kbnInfiniteScroll', createInfiniteScrollDirective) - .directive('docViewer', createDocViewerDirective); + .directive('docViewer', createDocViewerDirective) + .directive('contextAppLegacy', createContextAppLegacy); } diff --git a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx b/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx index 33724068a6ba8..551caa28d2291 100644 --- a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx +++ b/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx @@ -17,37 +17,45 @@ * under the License. */ -import * as React from 'react'; -import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { withKnobs, boolean } from '@storybook/addon-knobs'; +import * as React from 'react'; import { PanelOptionsMenu } from '..'; -const euiContextDescriptors = { - id: 'mainMenu', - title: 'Options', - items: [ - { - name: 'Inspect', - icon: 'inspect', - onClick: action('onClick(inspect)'), - }, - { - name: 'Full screen', - icon: 'expand', - onClick: action('onClick(expand)'), +export default { + title: 'components/PanelOptionsMenu', + component: PanelOptionsMenu, + argTypes: { + isViewMode: { + control: { type: 'boolean' }, }, + }, + decorators: [ + (Story: React.ComponentType) => ( +
+ +
+ ), ], }; -storiesOf('components/PanelOptionsMenu', module) - .addDecorator(withKnobs) - .add('default', () => { - const isViewMode = boolean('isViewMode', false); +export function Default({ isViewMode }: React.ComponentProps) { + const euiContextDescriptors = { + id: 'mainMenu', + title: 'Options', + items: [ + { + name: 'Inspect', + icon: 'inspect', + onClick: action('onClick(inspect)'), + }, + { + name: 'Full screen', + icon: 'expand', + onClick: action('onClick(expand)'), + }, + ], + }; - return ( -
- -
- ); - }); + return ; +} +Default.args = { isViewMode: false } as React.ComponentProps; diff --git a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss index cdc0f9f0e0451..6b8654f6c3528 100644 --- a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss +++ b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss @@ -64,12 +64,12 @@ .embPanel__titleText { @include euiTextTruncate; + font-weight: $euiFontWeightBold; } .embPanel__placeholderTitleText { - @include euiTextTruncate; - font-weight: $euiFontWeightRegular; color: $euiColorMediumShade; + font-weight: $euiFontWeightRegular; } } 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 fcf79c1d6b211..c717e4370231e 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { nextTick } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; @@ -343,6 +343,88 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => { // expect(action.length).toBe(1); }); +test('Panel title customize link does not exist in view mode', async () => { + const inspector = inspectorPluginMock.createStartContract(); + + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false }, + { getEmbeddableFactory } as any + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Vayon', + lastName: 'Poole', + }); + + const component = mountWithIntl( + Promise.resolve([])} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} + notifications={{} as any} + overlays={{} as any} + application={applicationMock} + inspector={inspector} + SavedObjectFinder={() => null} + /> + ); + + const titleLink = findTestSubject(component, 'embeddablePanelTitleLink'); + expect(titleLink.length).toBe(0); +}); + +test('Runs customize panel action on title click when in edit mode', async () => { + const inspector = inspectorPluginMock.createStartContract(); + + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.EDIT, hidePanelTitles: false }, + { getEmbeddableFactory } as any + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Vayon', + lastName: 'Poole', + }); + + const component = mountWithIntl( + Promise.resolve([])} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} + notifications={{} as any} + overlays={{} as any} + application={applicationMock} + inspector={inspector} + SavedObjectFinder={() => null} + /> + ); + + const titleExecute = jest.fn(); + component.setState((s: any) => ({ + ...s, + universalActions: { + ...s.universalActions, + customizePanelTitle: { execute: titleExecute, isCompatible: jest.fn() }, + }, + })); + + const titleLink = findTestSubject(component, 'embeddablePanelTitleLink'); + expect(titleLink.length).toBe(1); + titleLink.simulate('click'); + await nextTick(); + expect(titleExecute).toHaveBeenCalledTimes(1); +}); + test('Updates when hidePanelTitles is toggled', async () => { const inspector = inspectorPluginMock.createStartContract(); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 137f8c24b1fae..1cd48e85469fd 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -76,6 +76,7 @@ interface Props { interface State { panels: EuiContextMenuPanelDescriptor[]; + universalActions: PanelUniversalActions; focusedPanelIndex?: string; viewMode: ViewMode; hidePanelTitle: boolean; @@ -86,6 +87,14 @@ interface State { error?: EmbeddableError; } +interface PanelUniversalActions { + customizePanelTitle: CustomizePanelTitleAction; + addPanel: AddPanelAction; + inspectPanel: InspectPanelAction; + removePanel: RemovePanelAction; + editPanel: EditPanelAction; +} + export class EmbeddablePanel extends React.Component { private embeddableRoot: React.RefObject; private parentSubscription?: Subscription; @@ -102,6 +111,7 @@ export class EmbeddablePanel extends React.Component { Boolean(embeddable.getInput()?.hidePanelTitles); this.state = { + universalActions: this.getUniversalActions(), panels: [], viewMode, hidePanelTitle, @@ -229,6 +239,7 @@ export class EmbeddablePanel extends React.Component { getActionContextMenuPanel={this.getActionContextMenuPanel} hidePanelTitle={this.state.hidePanelTitle} isViewMode={viewOnlyMode} + customizeTitle={this.state.universalActions.customizePanelTitle} closeContextMenu={this.state.closeContextMenu} title={title} badges={this.state.badges} @@ -267,17 +278,7 @@ export class EmbeddablePanel extends React.Component { } }; - private getActionContextMenuPanel = async () => { - let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { - embeddable: this.props.embeddable, - }); - - const { disabledActions } = this.props.embeddable.getInput(); - if (disabledActions) { - const removeDisabledActions = removeById(disabledActions); - regularActions = regularActions.filter(removeDisabledActions); - } - + private getUniversalActions = (): PanelUniversalActions => { const createGetUserData = (overlays: OverlayStart) => async function getUserData(context: { embeddable: IEmbeddable }) { return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => { @@ -299,27 +300,41 @@ export class EmbeddablePanel extends React.Component { }); }; - // These actions are exposed on the context menu for every embeddable, they bypass the trigger + // Universal actions are exposed on the context menu for every embeddable, they bypass the trigger // registry. - const extraActions: Array> = [ - new CustomizePanelTitleAction(createGetUserData(this.props.overlays)), - new AddPanelAction( + return { + customizePanelTitle: new CustomizePanelTitleAction(createGetUserData(this.props.overlays)), + addPanel: new AddPanelAction( this.props.getEmbeddableFactory, this.props.getAllEmbeddableFactories, this.props.overlays, this.props.notifications, this.props.SavedObjectFinder ), - new InspectPanelAction(this.props.inspector), - new RemovePanelAction(), - new EditPanelAction( + inspectPanel: new InspectPanelAction(this.props.inspector), + removePanel: new RemovePanelAction(), + editPanel: new EditPanelAction( this.props.getEmbeddableFactory, this.props.application, this.props.stateTransfer ), - ]; + }; + }; - const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); + private getActionContextMenuPanel = async () => { + let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { + embeddable: this.props.embeddable, + }); + + const { disabledActions } = this.props.embeddable.getInput(); + if (disabledActions) { + const removeDisabledActions = removeById(disabledActions); + regularActions = regularActions.filter(removeDisabledActions); + } + + const sortedActions = [...regularActions, ...Object.values(this.state.universalActions)].sort( + sortByOrderField + ); return await buildContextMenuForActions({ actions: sortedActions.map((action) => ({ diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 9bcef051a9359..44f5c3df2709d 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -24,6 +24,7 @@ import { EuiToolTip, EuiScreenReaderOnly, EuiNotificationBadge, + EuiLink, } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; @@ -32,6 +33,7 @@ import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers'; import { uiToReactComponent } from '../../../../../kibana_react/public'; +import { CustomizePanelTitleAction } from '.'; export interface PanelHeaderProps { title?: string; @@ -44,6 +46,7 @@ export interface PanelHeaderProps { embeddable: IEmbeddable; headerId?: string; showPlaceholderTitle?: boolean; + customizeTitle: CustomizePanelTitleAction; } function renderBadges(badges: Array>, embeddable: IEmbeddable) { @@ -129,6 +132,7 @@ export function PanelHeader({ notifications, embeddable, headerId, + customizeTitle, }: PanelHeaderProps) { const description = getViewDescription(embeddable); const showTitle = !hidePanelTitle && (!isViewMode || title); @@ -172,11 +176,35 @@ export function PanelHeader({ } const renderTitle = () => { - const titleComponent = showTitle ? ( - - {title || placeholderTitle} - - ) : undefined; + let titleComponent; + if (showTitle) { + titleComponent = isViewMode ? ( + + {title || placeholderTitle} + + ) : ( + customizeTitle.execute({ embeddable })} + > + {title || placeholderTitle} + + ); + } return description ? ( error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > ', () => { find('btn').simulate('click').update(); }); - expect(onFormData.mock.calls.length).toBe(1); + expect(onFormData.mock.calls.length).toBe(2); const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< OnUpdateHandler diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index ac141baf8fc71..de4edc1edf873 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -22,13 +22,16 @@ import React from 'react'; import { FormData } from '../types'; import { useFormData } from '../hooks'; -interface Props { - children: (formData: FormData) => JSX.Element | null; +interface Props { + children: (formData: I) => JSX.Element | null; pathsToWatch?: string | string[]; } -export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { - const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); +const FormDataProviderComp = function ({ + children, + pathsToWatch, +}: Props) { + const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); if (!isReady) { // No field has mounted yet, don't render anything @@ -36,4 +39,6 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = } return children(formData); -}); +}; + +export const FormDataProvider = React.memo(FormDataProviderComp) as typeof FormDataProviderComp; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts index 3c4f9799bb1bc..812a18680d6b8 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -107,7 +107,7 @@ export const UseArray = ({ getNewItemAtIndex, ]); - // Create a new hook field with the "hasValue" set to false so we don't use its value to build the final form data. + // Create a new hook field with the "isIncludedInOutput" set to false so we don't use its value to build the final form data. // Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle. const fieldConfigBase: FieldConfig & InternalFieldConfig = { defaultValue: fieldDefaultValue, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index dbf53a9f0a359..1a7f8832e4a4e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -58,7 +58,7 @@ describe('', () => { OnUpdateHandler >; - expect(data.raw).toEqual({ + expect(data.internal).toEqual({ name: 'John', lastName: 'Snow', }); @@ -214,8 +214,8 @@ describe('', () => { expect(serializer).not.toBeCalled(); expect(formatter).not.toBeCalled(); - let formData = formHook.getFormData({ unflatten: false }); - expect(formData.name).toEqual('John-deserialized'); + const internalFormData = formHook.__getFormData$().value; + expect(internalFormData.name).toEqual('John-deserialized'); await act(async () => { form.setInputValue('myField', 'Mike'); @@ -224,9 +224,9 @@ describe('', () => { expect(formatter).toBeCalled(); // Formatters are executed on each value change expect(serializer).not.toBeCalled(); // Serializer are executed *only** when outputting the form data - formData = formHook.getFormData(); + const outputtedFormData = formHook.getFormData(); expect(serializer).toBeCalled(); - expect(formData.name).toEqual('MIKE-serialized'); + expect(outputtedFormData.name).toEqual('MIKE-serialized'); // Make sure that when we reset the form values, we don't serialize the fields serializer.mockReset(); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx index 0670220ccd0c9..6aef6d2b0d46a 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx @@ -22,9 +22,9 @@ import React, { createContext, useContext, useMemo } from 'react'; import { FormData, FormHook } from './types'; import { Subject } from './lib'; -export interface Context { - getFormData$: () => Subject; - getFormData: FormHook['getFormData']; +export interface Context { + getFormData$: () => Subject; + getFormData: FormHook['getFormData']; } const FormDataContext = createContext | undefined>(undefined); @@ -45,6 +45,6 @@ export const FormDataContextProvider = ({ children, getFormData$, getFormData }: return {children}; }; -export function useFormDataContext() { - return useContext | undefined>(FormDataContext); +export function useFormDataContext() { + return useContext | undefined>(FormDataContext); } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 7b21b6638aeac..f4f13a698ee30 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -63,6 +63,7 @@ export const useField = ( __removeField, __updateFormDataAt, __validateFields, + __getFormData$, } = form; const deserializeValue = useCallback( @@ -76,7 +77,7 @@ export const useField = ( ); const [value, setStateValue] = useState(deserializeValue); - const [errors, setErrors] = useState([]); + const [errors, setStateErrors] = useState([]); const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); const [isChangingValue, setIsChangingValue] = useState(false); @@ -86,18 +87,12 @@ export const useField = ( const validateCounter = useRef(0); const changeCounter = useRef(0); const hasBeenReset = useRef(false); - const inflightValidation = useRef | null>(null); + const inflightValidation = useRef<(Promise & { cancel?(): void }) | null>(null); const debounceTimeout = useRef(null); + // ---------------------------------- // -- HELPERS // ---------------------------------- - const serializeValue: FieldHook['__serializeValue'] = useCallback( - (internalValue: I = value) => { - return serializer ? serializer(internalValue) : ((internalValue as unknown) as T); - }, - [serializer, value] - ); - /** * Filter an array of errors with specific validation type on them * @@ -117,6 +112,11 @@ export const useField = ( ); }; + /** + * If the field has some "formatters" defined in its config, run them in series and return + * the transformed value. This handler is called whenever the field value changes, right before + * updating the "value" state. + */ const formatInputValue = useCallback( (inputValue: unknown): T => { const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === ''; @@ -125,11 +125,11 @@ export const useField = ( return inputValue as T; } - const formData = getFormData({ unflatten: false }); + const formData = __getFormData$().value; return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T; }, - [formatters, getFormData] + [formatters, __getFormData$] ); const onValueChange = useCallback(async () => { @@ -147,7 +147,7 @@ export const useField = ( // Update the form data observable __updateFormDataAt(path, value); - // Validate field(s) (that will update form.isValid state) + // Validate field(s) (this will update the form.isValid state) await __validateFields(fieldsToValidateOnChange ?? [path]); if (isMounted.current === false) { @@ -162,15 +162,18 @@ export const useField = ( */ if (changeIteration === changeCounter.current) { if (valueChangeDebounceTime > 0) { - const delta = Date.now() - startTime; - if (delta < valueChangeDebounceTime) { + const timeElapsed = Date.now() - startTime; + + if (timeElapsed < valueChangeDebounceTime) { + const timeLeftToWait = valueChangeDebounceTime - timeElapsed; debounceTimeout.current = setTimeout(() => { debounceTimeout.current = null; setIsChangingValue(false); - }, valueChangeDebounceTime - delta); + }, timeLeftToWait); return; } } + setIsChangingValue(false); } }, [ @@ -183,41 +186,34 @@ export const useField = ( __validateFields, ]); + // Cancel any inflight validation (e.g an HTTP Request) const cancelInflightValidation = useCallback(() => { - // Cancel any inflight validation (like an HTTP Request) - if ( - inflightValidation.current && - typeof (inflightValidation.current as any).cancel === 'function' - ) { - (inflightValidation.current as any).cancel(); + if (inflightValidation.current && typeof inflightValidation.current.cancel === 'function') { + inflightValidation.current.cancel(); inflightValidation.current = null; } }, []); - const clearErrors: FieldHook['clearErrors'] = useCallback( - (validationType = VALIDATION_TYPES.FIELD) => { - setErrors((previousErrors) => filterErrors(previousErrors, validationType)); - }, - [] - ); - const runValidations = useCallback( - ({ - formData, - value: valueToValidate, - validationTypeToValidate, - }: { - formData: any; - value: I; - validationTypeToValidate?: string; - }): ValidationError[] | Promise => { + ( + { + formData, + value: valueToValidate, + validationTypeToValidate, + }: { + formData: any; + value: I; + validationTypeToValidate?: string; + }, + clearFieldErrors: FieldHook['clearErrors'] + ): ValidationError[] | Promise => { if (!validations) { return []; } // By default, for fields that have an asynchronous validation // we will clear the errors as soon as the field value changes. - clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); + clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); cancelInflightValidation(); @@ -329,21 +325,33 @@ export const useField = ( // We first try to run the validations synchronously return runSync(); }, - [clearErrors, cancelInflightValidation, validations, getFormData, getFields, path] + [cancelInflightValidation, validations, getFormData, getFields, path] ); - // -- API // ---------------------------------- + // -- Internal API + // ---------------------------------- + const serializeValue: FieldHook['__serializeValue'] = useCallback( + (internalValue: I = value) => { + return serializer ? serializer(internalValue) : ((internalValue as unknown) as T); + }, + [serializer, value] + ); + + // ---------------------------------- + // -- Public API + // ---------------------------------- + const clearErrors: FieldHook['clearErrors'] = useCallback( + (validationType = VALIDATION_TYPES.FIELD) => { + setStateErrors((previousErrors) => filterErrors(previousErrors, validationType)); + }, + [] + ); - /** - * Validate a form field, running all its validations. - * If a validationType is provided then only that validation will be executed, - * skipping the other type of validation that might exist. - */ const validate: FieldHook['validate'] = useCallback( (validationData = {}) => { const { - formData = getFormData({ unflatten: false }), + formData = __getFormData$().value, value: valueToValidate = value, validationType, } = validationData; @@ -362,7 +370,7 @@ export const useField = ( // This is the most recent invocation setValidating(false); // Update the errors array - setErrors((prev) => { + setStateErrors((prev) => { const filteredErrors = filterErrors(prev, validationType); return [...filteredErrors, ..._validationErrors]; }); @@ -374,25 +382,23 @@ export const useField = ( }; }; - const validationErrors = runValidations({ - formData, - value: valueToValidate, - validationTypeToValidate: validationType, - }); + const validationErrors = runValidations( + { + formData, + value: valueToValidate, + validationTypeToValidate: validationType, + }, + clearErrors + ); if (Reflect.has(validationErrors, 'then')) { return (validationErrors as Promise).then(onValidationResult); } return onValidationResult(validationErrors as ValidationError[]); }, - [getFormData, value, runValidations] + [__getFormData$, value, runValidations, clearErrors] ); - /** - * Handler to change the field value - * - * @param newValue The new value to assign to the field - */ const setValue: FieldHook['setValue'] = useCallback( (newValue) => { setStateValue((prev) => { @@ -408,8 +414,8 @@ export const useField = ( [formatInputValue] ); - const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { - setErrors( + const setErrors: FieldHook['setErrors'] = useCallback((_errors) => { + setStateErrors( _errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, __isBlocking__: true, @@ -418,11 +424,6 @@ export const useField = ( ); }, []); - /** - * Form "onChange" event handler - * - * @param event Form input change event - */ const onChange: FieldHook['onChange'] = useCallback( (event) => { const newValue = {}.hasOwnProperty.call(event!.target, 'checked') @@ -485,7 +486,7 @@ export const useField = ( case 'value': return setValue(nextValue); case 'errors': - return setErrors(nextValue); + return setStateErrors(nextValue); case 'isChangingValue': return setIsChangingValue(nextValue); case 'isPristine': @@ -539,7 +540,7 @@ export const useField = ( onChange, getErrorsMessages, setValue, - setErrors: _setErrors, + setErrors, clearErrors, validate, reset, @@ -563,7 +564,7 @@ export const useField = ( onChange, getErrorsMessages, setValue, - _setErrors, + setErrors, clearErrors, validate, reset, @@ -585,7 +586,8 @@ export const useField = ( useEffect(() => { // If the field value has been reset, we don't want to call the "onValueChange()" - // as it will set the "isPristine" state to true or validate the field, which initially we don't want. + // as it will set the "isPristine" state to true or validate the field, which we don't want + // to occur right after resetting the field state. if (hasBeenReset.current) { hasBeenReset.current = false; return; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index b28c09d07fa98..9626aaa9b2459 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -211,7 +211,13 @@ describe('useForm() hook', () => { test('should allow subscribing to the form data changes and provide a handler to build the form data', async () => { const TestComp = ({ onData }: { onData: OnUpdateHandler }) => { - const { form } = useForm(); + const { form } = useForm({ + serializer: (value) => ({ + user: { + name: value.user.name.toUpperCase(), + }, + }), + }); const { subscribe } = form; useEffect(() => { @@ -253,8 +259,9 @@ describe('useForm() hook', () => { OnUpdateHandler >; - expect(data.raw).toEqual({ 'user.name': 'John' }); - expect(data.format()).toEqual({ user: { name: 'John' } }); + expect(data.internal).toEqual({ user: { name: 'John' } }); + // Transform name to uppercase as decalred in our serializer func + expect(data.format()).toEqual({ user: { name: 'JOHN' } }); // As we have touched all fields, the validity went from "undefined" to "true" expect(isValid).toBe(true); }); @@ -302,10 +309,12 @@ describe('useForm() hook', () => { OnUpdateHandler >; - expect(data.raw).toEqual({ + expect(data.internal).toEqual({ title: defaultValue.title, subTitle: 'hasBeenOverridden', - 'user.name': defaultValue.user.name, + user: { + name: defaultValue.user.name, + }, }); }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index be4535fec3669..869d1fac54b1e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -58,8 +58,6 @@ export function useForm( return initDefaultValue(defaultValue); }, [defaultValue, initDefaultValue]); - const defaultValueDeserialized = useRef(defaultValueMemoized); - const { valueChangeDebounceTime, stripEmptyFields: doStripEmptyFields } = options ?? {}; const formOptions = useMemo( () => ({ @@ -72,26 +70,36 @@ export function useForm( const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); const [isValid, setIsValid] = useState(undefined); + const fieldsRefs = useRef({}); const formUpdateSubscribers = useRef([]); const isMounted = useRef(false); + const defaultValueDeserialized = useRef(defaultValueMemoized); // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React // render(). - // The component is the one in charge of reading this observable - // and updating its state to trigger the necessary view render. - const formData$ = useRef | null>(null); + // The "useFormData()" hook is the one in charge of reading this observable + // and updating its own state that will trigger the necessary re-renders in the UI. + const formData$ = useRef | null>(null); + // ---------------------------------- // -- HELPERS // ---------------------------------- - const getFormData$ = useCallback((): Subject => { + const getFormData$ = useCallback((): Subject => { if (formData$.current === null) { - formData$.current = new Subject({} as T); + formData$.current = new Subject({}); } return formData$.current; }, []); + const updateFormData$ = useCallback( + (nextValue: FormData) => { + getFormData$().next(nextValue); + }, + [getFormData$] + ); + const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []); const getFieldsForOutput = useCallback( @@ -115,63 +123,24 @@ export function useForm( [] ); - const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback( + const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback( (path, value) => { - const _formData$ = getFormData$(); - const currentFormData = _formData$.value; + const currentFormData = getFormData$().value; if (currentFormData[path] !== value) { - _formData$.next({ ...currentFormData, [path]: value }); + updateFormData$({ ...currentFormData, [path]: value }); } - - return _formData$.value; }, - [getFormData$] + [getFormData$, updateFormData$] ); - const updateDefaultValueAt: FormHook['__updateDefaultValueAt'] = useCallback((path, value) => { - set(defaultValueDeserialized.current, path, value); - }, []); - - // -- API - // ---------------------------------- - const getFormData: FormHook['getFormData'] = useCallback( - (getDataOptions: Parameters['getFormData']>[0] = { unflatten: true }) => { - if (getDataOptions.unflatten) { - const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, { - stripEmptyFields: formOptions.stripEmptyFields, - }); - const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); - return serializer - ? (serializer(unflattenObject(fieldsValue) as I) as T) - : (unflattenObject(fieldsValue) as T); - } - - return Object.entries(fieldsRefs.current).reduce( - (acc, [key, field]) => ({ - ...acc, - [key]: field.value, - }), - {} as T - ); + const updateDefaultValueAt: FormHook['__updateDefaultValueAt'] = useCallback( + (path, value) => { + set(defaultValueDeserialized.current, path, value); }, - [getFieldsForOutput, formOptions.stripEmptyFields, serializer] + [] ); - const getErrors: FormHook['getErrors'] = useCallback(() => { - if (isValid === true) { - return []; - } - - return fieldsToArray().reduce((acc, field) => { - const fieldError = field.getErrorsMessages(); - if (fieldError === null) { - return acc; - } - return [...acc, fieldError]; - }, [] as string[]); - }, [isValid, fieldsToArray]); - const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating; const waitForFieldsToFinishValidating = useCallback(async () => { @@ -192,13 +161,13 @@ export function useForm( }); }, [fieldsToArray]); - const validateFields: FormHook['__validateFields'] = useCallback( + const validateFields: FormHook['__validateFields'] = useCallback( async (fieldNames) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); - const formData = getFormData({ unflatten: false }); + const formData = getFormData$().value; const validationResult = await Promise.all( fieldsToValidate.map((field) => field.validate({ formData })) ); @@ -245,34 +214,13 @@ export function useForm( return { areFieldsValid, isFormValid }; }, - [getFormData, fieldsToArray] + [getFormData$, fieldsToArray] ); - const validateAllFields = useCallback(async (): Promise => { - // Maybe some field are being validated because of their async validation(s). - // We make sure those validations have finished executing before proceeding. - await waitForFieldsToFinishValidating(); - - if (!isMounted.current) { - return false; - } - - const fieldsArray = fieldsToArray(); - const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated); - - let isFormValid: boolean | undefined; - - if (fieldsToValidate.length === 0) { - isFormValid = fieldsArray.every(isFieldValid); - } else { - ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); - } - - setIsValid(isFormValid); - return isFormValid!; - }, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]); - - const addField: FormHook['__addField'] = useCallback( + // ---------------------------------- + // -- Internal API + // ---------------------------------- + const addField: FormHook['__addField'] = useCallback( (field) => { fieldsRefs.current[field.path] = field; @@ -291,17 +239,17 @@ export function useForm( [updateFormDataAt] ); - const removeField: FormHook['__removeField'] = useCallback( + const removeField: FormHook['__removeField'] = useCallback( (_fieldNames) => { const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames]; - const currentFormData = { ...getFormData$().value } as FormData; + const currentFormData = { ...getFormData$().value }; fieldNames.forEach((name) => { delete fieldsRefs.current[name]; delete currentFormData[name]; }); - getFormData$().next(currentFormData as T); + updateFormData$(currentFormData); /** * After removing a field, the form validity might have changed @@ -316,40 +264,91 @@ export function useForm( return prev; }); }, - [getFormData$, fieldsToArray] + [getFormData$, updateFormData$, fieldsToArray] + ); + + const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback( + (fieldName) => get(defaultValueDeserialized.current, fieldName), + [] + ); + + const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback( + (fieldName) => { + const config = (get(schema ?? {}, fieldName) as FieldConfig) || {}; + + return config; + }, + [schema] ); - const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => { + // ---------------------------------- + // -- Public API + // ---------------------------------- + const getFormData: FormHook['getFormData'] = useCallback(() => { + const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, { + stripEmptyFields: formOptions.stripEmptyFields, + }); + const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); + return serializer + ? serializer(unflattenObject(fieldsValue)) + : unflattenObject(fieldsValue); + }, [getFieldsForOutput, formOptions.stripEmptyFields, serializer]); + + const getErrors: FormHook['getErrors'] = useCallback(() => { + if (isValid === true) { + return []; + } + + return fieldsToArray().reduce((acc, field) => { + const fieldError = field.getErrorsMessages(); + if (fieldError === null) { + return acc; + } + return [...acc, fieldError]; + }, [] as string[]); + }, [isValid, fieldsToArray]); + + const validate: FormHook['validate'] = useCallback(async (): Promise => { + // Maybe some field are being validated because of their async validation(s). + // We make sure those validations have finished executing before proceeding. + await waitForFieldsToFinishValidating(); + + if (!isMounted.current) { + return false; + } + + const fieldsArray = fieldsToArray(); + const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated); + + let isFormValid: boolean | undefined; + + if (fieldsToValidate.length === 0) { + isFormValid = fieldsArray.every(isFieldValid); + } else { + ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); + } + + setIsValid(isFormValid); + return isFormValid!; + }, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]); + + const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => { if (fieldsRefs.current[fieldName] === undefined) { return; } fieldsRefs.current[fieldName].setValue(value); }, []); - const setFieldErrors: FormHook['setFieldErrors'] = useCallback((fieldName, errors) => { + const setFieldErrors: FormHook['setFieldErrors'] = useCallback((fieldName, errors) => { if (fieldsRefs.current[fieldName] === undefined) { return; } fieldsRefs.current[fieldName].setErrors(errors); }, []); - const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []); - - const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback( - (fieldName) => get(defaultValueDeserialized.current, fieldName), - [] - ); - - const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback( - (fieldName) => { - const config = (get(schema ?? {}, fieldName) as FieldConfig) || {}; + const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []); - return config; - }, - [schema] - ); - - const submitForm: FormHook['submit'] = useCallback( + const submit: FormHook['submit'] = useCallback( async (e) => { if (e) { e.preventDefault(); @@ -358,7 +357,7 @@ export function useForm( setIsSubmitted(true); // User has attempted to submit the form at least once setSubmitting(true); - const isFormValid = await validateAllFields(); + const isFormValid = await validate(); const formData = isFormValid ? getFormData() : ({} as T); if (onSubmit) { @@ -371,13 +370,17 @@ export function useForm( return { data: formData, isValid: isFormValid! }; }, - [validateAllFields, getFormData, onSubmit] + [validate, getFormData, onSubmit] ); - const subscribe: FormHook['subscribe'] = useCallback( + const subscribe: FormHook['subscribe'] = useCallback( (handler) => { const subscription = getFormData$().subscribe((raw) => { - handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); + handler({ + isValid, + data: { internal: unflattenObject(raw), format: getFormData }, + validate, + }); }); formUpdateSubscribers.current.push(subscription); @@ -391,17 +394,13 @@ export function useForm( }, }; }, - [getFormData$, isValid, getFormData, validateAllFields] + [getFormData$, isValid, getFormData, validate] ); - /** - * Reset all the fields of the form to their default values - * and reset all the states to their original value. - */ - const reset: FormHook['reset'] = useCallback( + const reset: FormHook['reset'] = useCallback( (resetOptions = { resetValues: true }) => { const { resetValues = true, defaultValue: updatedDefaultValue } = resetOptions; - const currentFormData = { ...getFormData$().value } as FormData; + const currentFormData = { ...getFormData$().value }; if (updatedDefaultValue) { defaultValueDeserialized.current = initDefaultValue(updatedDefaultValue); @@ -417,25 +416,26 @@ export function useForm( currentFormData[path] = fieldDefaultValue; } }); + if (resetValues) { - getFormData$().next(currentFormData as T); + updateFormData$(currentFormData); } setIsSubmitted(false); setSubmitting(false); setIsValid(undefined); }, - [getFormData$, initDefaultValue, getFieldDefaultValue] + [getFormData$, updateFormData$, initDefaultValue, getFieldDefaultValue] ); - const form = useMemo>(() => { + const form = useMemo>(() => { return { isSubmitted, isSubmitting, isValid, id, - submit: submitForm, - validate: validateAllFields, + submit, + validate, subscribe, setFieldValue, setFieldErrors, @@ -458,7 +458,7 @@ export function useForm( isSubmitting, isValid, id, - submitForm, + submit, subscribe, setFieldValue, setFieldErrors, @@ -475,7 +475,7 @@ export function useForm( addField, removeField, validateFields, - validateAllFields, + validate, ]); useEffect(() => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx index 0fb65daecf2f4..beb8e58edbf49 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed } from '../shared_imports'; @@ -25,37 +25,59 @@ import { Form, UseField } from '../components'; import { useForm } from './use_form'; import { useFormData, HookReturn } from './use_form_data'; -interface Props { - onChange(data: HookReturn): void; +interface Props { + onChange(data: HookReturn): void; watch?: string | string[]; } +interface Form1 { + title: string; +} + +interface Form2 { + user: { + firstName: string; + lastName: string; + }; +} + +interface Form3 { + title: string; + subTitle: string; +} + describe('useFormData() hook', () => { - const HookListenerComp = React.memo(({ onChange, watch }: Props) => { - const hookValue = useFormData({ watch }); + const HookListenerComp = function ({ onChange, watch }: Props) { + const hookValue = useFormData({ watch }); + const isMounted = useRef(false); useEffect(() => { - onChange(hookValue); + if (isMounted.current) { + onChange(hookValue); + } + isMounted.current = true; }, [hookValue, onChange]); return null; - }); + }; + + const HookListener = React.memo(HookListenerComp); describe('form data updates', () => { let testBed: TestBed; let onChangeSpy: jest.Mock; const getLastMockValue = () => { - return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = (props: Props) => { - const { form } = useForm(); + const TestComp = (props: Props) => { + const { form } = useForm(); return (
- + ); }; @@ -70,9 +92,7 @@ describe('useFormData() hook', () => { }); test('should return the form data', () => { - // Called twice: - // once when the hook is called and once when the fields have mounted and updated the form data - expect(onChangeSpy).toBeCalledTimes(2); + expect(onChangeSpy).toBeCalledTimes(1); const [data] = getLastMockValue(); expect(data).toEqual({ title: 'titleInitialValue' }); }); @@ -86,7 +106,7 @@ describe('useFormData() hook', () => { setInputValue('titleField', 'titleChanged'); }); - expect(onChangeSpy).toBeCalledTimes(3); + expect(onChangeSpy).toBeCalledTimes(2); const [data] = getLastMockValue(); expect(data).toEqual({ title: 'titleChanged' }); }); @@ -96,17 +116,17 @@ describe('useFormData() hook', () => { let onChangeSpy: jest.Mock; const getLastMockValue = () => { - return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = (props: Props) => { - const { form } = useForm(); + const TestComp = (props: Props) => { + const { form } = useForm(); return (
- + ); }; @@ -121,8 +141,8 @@ describe('useFormData() hook', () => { }); test('should expose a handler to build the form data', () => { - const { 1: format } = getLastMockValue(); - expect(format()).toEqual({ + const [formData] = getLastMockValue(); + expect(formData).toEqual({ user: { firstName: 'John', lastName: 'Snow', @@ -137,11 +157,11 @@ describe('useFormData() hook', () => { let onChangeSpy: jest.Mock; const getLastMockValue = () => { - return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = (props: Props) => { - const { form } = useForm(); + const TestComp = (props: Props) => { + const { form } = useForm(); return (
@@ -190,9 +210,9 @@ describe('useFormData() hook', () => { return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = ({ onChange }: Props) => { + const TestComp = ({ onChange }: Props) => { const { form } = useForm(); - const hookValue = useFormData({ form }); + const hookValue = useFormData({ form }); useEffect(() => { onChange(hookValue); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts index 6c6dee3624979..9487e2d30c680 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -19,6 +19,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { FormData, FormHook } from '../types'; +import { unflattenObject } from '../lib'; import { useFormDataContext, Context } from '../form_data_context'; interface Options { @@ -26,14 +27,16 @@ interface Options { form?: FormHook; } -export type HookReturn = [FormData, () => T, boolean]; +export type HookReturn = [I, () => T, boolean]; -export const useFormData = (options: Options = {}): HookReturn => { +export const useFormData = ( + options: Options = {} +): HookReturn => { const { watch, form } = options; - const ctx = useFormDataContext(); + const ctx = useFormDataContext(); - let getFormData: Context['getFormData']; - let getFormData$: Context['getFormData$']; + let getFormData: Context['getFormData']; + let getFormData$: Context['getFormData$']; if (form !== undefined) { getFormData = form.getFormData; @@ -50,30 +53,33 @@ export const useFormData = (options: Options = {}): const previousRawData = useRef(initialValue); const isMounted = useRef(false); - const [formData, setFormData] = useState(previousRawData.current); + const [formData, setFormData] = useState(() => unflattenObject(previousRawData.current)); - const formatFormData = useCallback(() => { - return getFormData({ unflatten: true }); - }, [getFormData]); + /** + * We do want to offer to the consumer a handler to serialize the form data that changes each time + * the formData **state** changes. This is why we added the "formData" dep to the array and added the eslint override. + */ + const serializer = useCallback(() => { + return getFormData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getFormData, formData]); useEffect(() => { const subscription = getFormData$().subscribe((raw) => { + if (!isMounted.current && Object.keys(raw).length === 0) { + return; + } + if (watch) { - const valuesToWatchArray = Array.isArray(watch) - ? (watch as string[]) - : ([watch] as string[]); + const pathsToWatchArray: string[] = Array.isArray(watch) ? watch : [watch]; - if ( - valuesToWatchArray.some( - (value) => previousRawData.current[value] !== raw[value as keyof T] - ) - ) { + if (pathsToWatchArray.some((path) => previousRawData.current[path] !== raw[path])) { previousRawData.current = raw; // Only update the state if one of the field we watch has changed. - setFormData(raw); + setFormData(unflattenObject(raw)); } } else { - setFormData(raw); + setFormData(unflattenObject(raw)); } }); return subscription.unsubscribe; @@ -88,8 +94,8 @@ export const useFormData = (options: Options = {}): if (!isMounted.current && Object.keys(formData).length === 0) { // No field has mounted yet - return [formData, formatFormData, false]; + return [formData, serializer, false]; } - return [formData, formatFormData, true]; + return [formData, serializer, true]; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts index 7d506e28794fd..f67070c8746a1 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -20,25 +20,11 @@ import { set } from '@elastic/safer-lodash-set'; import { FieldHook } from '../types'; -export const unflattenObject = (object: any) => +export const unflattenObject = (object: object): T => Object.entries(object).reduce((acc, [key, value]) => { set(acc, key, value); return acc; - }, {}); - -export const flattenObject = ( - object: Record, - to: Record = {}, - paths: string[] = [] -): Record => - Object.entries(object).reduce((acc, [key, value]) => { - const updatedPaths = [...paths, key]; - if (value !== null && !Array.isArray(value) && typeof value === 'object') { - return flattenObject(value, to, updatedPaths); - } - acc[updatedPaths.join('.')] = value; - return acc; - }, to); + }, {} as T); /** * Helper to map the object of fields to any of its value diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index ae731caff2881..6196ae83a84a6 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -40,33 +40,36 @@ export interface FormHook submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; /** Use this handler to get the validity of the form. */ validate: () => Promise; - subscribe: (handler: OnUpdateHandler) => Subscription; + subscribe: (handler: OnUpdateHandler) => Subscription; /** Sets a field value imperatively. */ setFieldValue: (fieldName: string, value: FieldValue) => void; /** Sets a field errors imperatively. */ setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; - /** Access any field on the form. */ + /** Access the fields on the form. */ getFields: () => FieldsMap; /** * Return the form data. It accepts an optional options object with an `unflatten` parameter (defaults to `true`). * If you are only interested in the raw form data, pass `unflatten: false` to the handler */ - getFormData: (options?: { unflatten?: boolean }) => T; + getFormData: () => T; /* Returns an array with of all errors in the form. */ getErrors: () => string[]; - /** Resets the form to its initial state. */ + /** + * Reset the form states to their initial value and optionally + * all the fields to their initial values. + */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; readonly __options: Required; - __getFormData$: () => Subject; + __getFormData$: () => Subject; __addField: (field: FieldHook) => void; __removeField: (fieldNames: string | string[]) => void; __validateFields: ( fieldNames: string[] ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; - __updateFormDataAt: (field: string, value: unknown) => T; + __updateFormDataAt: (field: string, value: unknown) => void; __updateDefaultValueAt: (field: string, value: unknown) => void; - __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; - __getFieldDefaultValue: (fieldName: string) => unknown; + __readFieldConfigFromSchema: (field: string) => FieldConfig; + __getFieldDefaultValue: (path: string) => unknown; } export type FormSchema = { @@ -83,16 +86,18 @@ export interface FormConfig { +export interface OnFormUpdateArg { data: { - raw: { [key: string]: any }; + internal: I; format: () => T; }; validate: () => Promise; isValid?: boolean; } -export type OnUpdateHandler = (arg: OnFormUpdateArg) => void; +export type OnUpdateHandler = ( + arg: OnFormUpdateArg +) => void; export interface FormOptions { valueChangeDebounceTime?: number; @@ -119,10 +124,26 @@ export interface FieldHook { validationType?: 'field' | string; errorCode?: string; }) => string | null; + /** + * Form "onChange" event handler + * + * @param event Form input change event + */ onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; + /** + * Handler to change the field value + * + * @param value The new value to assign to the field. If you provide a callback, you wil receive + * the previous value and you need to return the next value. + */ setValue: (value: I | ((prevValue: I) => I)) => void; setErrors: (errors: ValidationError[]) => void; clearErrors: (type?: string | string[]) => void; + /** + * Validate a form field, running all its validations. + * If a validationType is provided then only that validation will be executed, + * skipping the other type of validation that might exist. + */ validate: (validateData?: { formData?: any; value?: I; @@ -166,19 +187,23 @@ export interface ValidationError { [key: string]: any; } -export interface ValidationFuncArg { +export interface ValidationFuncArg { path: string; value: V; form: { - getFormData: FormHook['getFormData']; - getFields: FormHook['getFields']; + getFormData: FormHook['getFormData']; + getFields: FormHook['getFields']; }; - formData: T; + formData: I; errors: readonly ValidationError[]; } -export type ValidationFunc = ( - data: ValidationFuncArg +export type ValidationFunc< + I extends FormData = FormData, + E extends string = string, + V = unknown +> = ( + data: ValidationFuncArg ) => ValidationError | void | undefined | Promise | void | undefined>; export interface FieldValidateResponse { @@ -199,11 +224,11 @@ type FormatterFunc = (value: any, formData: FormData) => unknown; type FieldValue = unknown; export interface ValidationConfig< - FormType extends FormData = any, - Error extends string = string, - ValueType = unknown + I extends FormData = FormData, + E extends string = string, + V = unknown > { - validator: ValidationFunc; + validator: ValidationFunc; type?: string; /** * By default all validation are blockers, which means that if they fail, the field is invalid. diff --git a/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts new file mode 100644 index 0000000000000..970015638794f --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts @@ -0,0 +1,170 @@ +/* + * 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 { ExpressionFunctionDefinition } from '../types'; +import { Datatable, DatatableRow } from '../../expression_types'; + +export interface CumulativeSumArgs { + by?: string[]; + inputColumnId: string; + outputColumnId: string; + outputColumnName?: string; +} + +export type ExpressionFunctionCumulativeSum = ExpressionFunctionDefinition< + 'cumulative_sum', + Datatable, + CumulativeSumArgs, + Datatable +>; + +/** + * Returns a string identifying the group of a row by a list of columns to group by + */ +function getBucketIdentifier(row: DatatableRow, groupColumns?: string[]) { + return (groupColumns || []) + .map((groupColumnId) => (row[groupColumnId] == null ? '' : String(row[groupColumnId]))) + .join('|'); +} + +/** + * Calculates the cumulative sum of a specified column in the data table. + * + * Also supports multiple series in a single data table - use the `by` argument + * to specify the columns to split the calculation by. + * For each unique combination of all `by` columns a separate cumulative sum will be calculated. + * The order of rows won't be changed - this function is not modifying any existing columns, it's only + * adding the specified `outputColumnId` column to every row of the table without adding or removing rows. + * + * Behavior: + * * Will write the cumulative sum of `inputColumnId` into `outputColumnId` + * * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId` + * * Cumulative sums always start with 0, a cell will contain its own value plus the values of + * all cells of the same series further up in the table. + * + * Edge cases: + * * Will return the input table if `inputColumnId` does not exist + * * Will throw an error if `outputColumnId` exists already in provided data table + * * If the row value contains `null` or `undefined`, it will be ignored and overwritten with the cumulative sum of + * all cells of the same series further up in the table. + * * For all values besides `null` and `undefined`, the value will be cast to a number before it's added to the + * cumulative sum of the current series - if this results in `NaN` (like in case of objects), all cells of the + * current series will be set to `NaN`. + * * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings + * before comparison. If the values are objects, the return value of their `toString` method will be used for comparison. + * Missing values (`null` and `undefined`) will be treated as empty strings. + */ +export const cumulativeSum: ExpressionFunctionCumulativeSum = { + name: 'cumulative_sum', + type: 'datatable', + + inputTypes: ['datatable'], + + help: i18n.translate('expressions.functions.cumulativeSum.help', { + defaultMessage: 'Calculates the cumulative sum of a column in a data table', + }), + + args: { + by: { + help: i18n.translate('expressions.functions.cumulativeSum.args.byHelpText', { + defaultMessage: 'Column to split the cumulative sum calculation by', + }), + multi: true, + types: ['string'], + required: false, + }, + inputColumnId: { + help: i18n.translate('expressions.functions.cumulativeSum.args.inputColumnIdHelpText', { + defaultMessage: 'Column to calculate the cumulative sum of', + }), + types: ['string'], + required: true, + }, + outputColumnId: { + help: i18n.translate('expressions.functions.cumulativeSum.args.outputColumnIdHelpText', { + defaultMessage: 'Column to store the resulting cumulative sum in', + }), + types: ['string'], + required: true, + }, + outputColumnName: { + help: i18n.translate('expressions.functions.cumulativeSum.args.outputColumnNameHelpText', { + defaultMessage: 'Name of the column to store the resulting cumulative sum in', + }), + types: ['string'], + required: false, + }, + }, + + fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) { + if (input.columns.some((column) => column.id === outputColumnId)) { + throw new Error( + i18n.translate('expressions.functions.cumulativeSum.columnConflictMessage', { + defaultMessage: + 'Specified outputColumnId {columnId} already exists. Please pick another column id.', + values: { + columnId: outputColumnId, + }, + }) + ); + } + + const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId); + + if (!inputColumnDefinition) { + return input; + } + + const outputColumnDefinition = { + ...inputColumnDefinition, + id: outputColumnId, + name: outputColumnName || outputColumnId, + }; + + const resultColumns = [...input.columns]; + // add output column after input column in the table + resultColumns.splice( + resultColumns.indexOf(inputColumnDefinition) + 1, + 0, + outputColumnDefinition + ); + + const accumulators: Partial> = {}; + return { + ...input, + columns: resultColumns, + rows: input.rows.map((row) => { + const newRow = { ...row }; + + const bucketIdentifier = getBucketIdentifier(row, by); + const accumulatorValue = accumulators[bucketIdentifier] ?? 0; + const currentValue = newRow[inputColumnId]; + if (currentValue != null) { + newRow[outputColumnId] = Number(currentValue) + accumulatorValue; + accumulators[bucketIdentifier] = newRow[outputColumnId]; + } else { + newRow[outputColumnId] = accumulatorValue; + } + + return newRow; + }), + }; + }, +}; diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts index 5b9562dae5f2e..aadea5882b9c0 100644 --- a/src/plugins/expressions/common/expression_functions/specs/index.ts +++ b/src/plugins/expressions/common/expression_functions/specs/index.ts @@ -25,6 +25,7 @@ import { variableSet } from './var_set'; import { variable } from './var'; import { AnyExpressionFunctionDefinition } from '../types'; import { theme } from './theme'; +import { cumulativeSum } from './cumulative_sum'; export const functionSpecs: AnyExpressionFunctionDefinition[] = [ clog, @@ -34,6 +35,7 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [ variableSet, variable, theme, + cumulativeSum, ]; export * from './clog'; @@ -43,3 +45,4 @@ export * from './kibana_context'; export * from './var_set'; export * from './var'; export * from './theme'; +export * from './cumulative_sum'; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/cumulative_sum.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/cumulative_sum.test.ts new file mode 100644 index 0000000000000..037b3ddc25f89 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/cumulative_sum.test.ts @@ -0,0 +1,361 @@ +/* + * 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 { functionWrapper } from './utils'; +import { cumulativeSum, CumulativeSumArgs } from '../cumulative_sum'; +import { ExecutionContext } from '../../../execution/types'; +import { Datatable } from '../../../expression_types/specs/datatable'; + +describe('interpreter/functions#cumulative_sum', () => { + const fn = functionWrapper(cumulativeSum); + const runFn = (input: Datatable, args: CumulativeSumArgs) => + fn(input, args, {} as ExecutionContext) as Datatable; + + it('calculates cumulative sum', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: 7 }, { val: 3 }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]); + }); + + it('replaces null or undefined data with zeroes until there is real data', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{}, { val: null }, { val: undefined }, { val: 1 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([0, 0, 0, 1]); + }); + + it('calculates cumulative sum for multiple series', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3, split: 'B' }, + { val: 4, split: 'A' }, + { val: 5, split: 'A' }, + { val: 6, split: 'A' }, + { val: 7, split: 'B' }, + { val: 8, split: 'B' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + + expect(result.rows.map((row) => row.output)).toEqual([ + 1, + 2, + 2 + 3, + 1 + 4, + 1 + 4 + 5, + 1 + 4 + 5 + 6, + 2 + 3 + 7, + 2 + 3 + 7 + 8, + ]); + }); + + it('treats missing split column as separate series', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3 }, + { val: 4, split: 'A' }, + { val: 5 }, + { val: 6, split: 'A' }, + { val: 7, split: 'B' }, + { val: 8, split: 'B' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + expect(result.rows.map((row) => row.output)).toEqual([ + 1, + 2, + 3, + 1 + 4, + 3 + 5, + 1 + 4 + 6, + 2 + 7, + 2 + 7 + 8, + ]); + }); + + it('treats null like undefined and empty string for split columns', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3 }, + { val: 4, split: 'A' }, + { val: 5 }, + { val: 6, split: 'A' }, + { val: 7, split: null }, + { val: 8, split: 'B' }, + { val: 9, split: '' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + expect(result.rows.map((row) => row.output)).toEqual([ + 1, + 2, + 3, + 1 + 4, + 3 + 5, + 1 + 4 + 6, + 3 + 5 + 7, + 2 + 8, + 3 + 5 + 7 + 9, + ]); + }); + + it('calculates cumulative sum for multiple series by multiple split columns', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + { id: 'split2', name: 'split2', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A', split2: 'C' }, + { val: 2, split: 'B', split2: 'C' }, + { val: 3, split2: 'C' }, + { val: 4, split: 'A', split2: 'C' }, + { val: 5 }, + { val: 6, split: 'A', split2: 'D' }, + { val: 7, split: 'B', split2: 'D' }, + { val: 8, split: 'B', split2: 'D' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split', 'split2'] } + ); + expect(result.rows.map((row) => row.output)).toEqual([1, 2, 3, 1 + 4, 5, 6, 7, 7 + 8]); + }); + + it('splits separate series by the string representation of the cell values', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: { anObj: 3 } }, + { val: 2, split: { anotherObj: 5 } }, + { val: 10, split: 5 }, + { val: 11, split: '5' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + + expect(result.rows.map((row) => row.output)).toEqual([1, 1 + 2, 10, 21]); + }); + + it('casts values to number before calculating cumulative sum', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: '7' }, { val: '3' }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]); + }); + + it('casts values to number before calculating cumulative sum for NaN like values', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: '7' }, { val: {} }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.rows.map((row) => row.output)).toEqual([5, 12, NaN, NaN]); + }); + + it('skips undefined and null values', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [ + { val: null }, + { val: 7 }, + { val: undefined }, + { val: undefined }, + { val: undefined }, + { val: undefined }, + { val: '3' }, + { val: 2 }, + { val: null }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.rows.map((row) => row.output)).toEqual([0, 7, 7, 7, 7, 7, 10, 12, 12]); + }); + + it('copies over meta information from the source column', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + + field: 'afield', + index: 'anindex', + params: { id: 'number', params: { pattern: '000' } }, + source: 'synthetic', + sourceParams: { + some: 'params', + }, + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { + type: 'number', + + field: 'afield', + index: 'anindex', + params: { id: 'number', params: { pattern: '000' } }, + source: 'synthetic', + sourceParams: { + some: 'params', + }, + }, + }); + }); + + it('sets output name on output column if specified', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', outputColumnName: 'Output name' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'Output name', + meta: { type: 'number' }, + }); + }); + + it('returns source table if input column does not exist', () => { + const input: Datatable = { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }; + expect(runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(input); + }); + + it('throws an error if output column exists already', () => { + expect(() => + runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'val' } + ) + ).toThrow(); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index caaef541aefd5..fb1823e85b391 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -29,6 +29,7 @@ import { ExpressionFunctionVarSet, ExpressionFunctionVar, ExpressionFunctionTheme, + ExpressionFunctionCumulativeSum, } from './specs'; import { ExpressionAstFunction } from '../ast'; import { PersistableStateDefinition } from '../../../kibana_utils/common'; @@ -131,4 +132,5 @@ export interface ExpressionFunctionDefinitions { var_set: ExpressionFunctionVarSet; var: ExpressionFunctionVar; theme: ExpressionFunctionTheme; + cumulative_sum: ExpressionFunctionCumulativeSum; } diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 95ee651d433ac..4739b9434bdaa 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -377,6 +377,10 @@ export interface ExpressionFunctionDefinitions { // // (undocumented) clog: ExpressionFunctionClog; + // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionCumulativeSum" needs to be exported by the entry point index.d.ts + // + // (undocumented) + cumulative_sum: ExpressionFunctionCumulativeSum; // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionFont" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index d5da60af8f8e5..fcdfd5ef3246c 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -349,6 +349,10 @@ export interface ExpressionFunctionDefinitions { // // (undocumented) clog: ExpressionFunctionClog; + // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionCumulativeSum" needs to be exported by the entry point index.d.ts + // + // (undocumented) + cumulative_sum: ExpressionFunctionCumulativeSum; // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionFont" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap index 9a9e055d54d2f..9992fe2c8cf5f 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap @@ -526,7 +526,7 @@ exports[`bulkCreate should display success message when bulkCreate is successful size="m" type="check" > -
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index 45253f6ad27c0..8e7fac9c6c148 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -6,6 +6,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` editField={[Function]} indexPattern={ Object { + "getFormatterForFieldNoDefault": [Function], "getNonScriptedFields": [Function], } } @@ -14,7 +15,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` Object { "displayName": "Elastic", "excluded": false, - "format": undefined, + "format": "", "info": Array [], "name": "Elastic", "searchable": true, @@ -32,6 +33,7 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` editField={[Function]} indexPattern={ Object { + "getFormatterForFieldNoDefault": [Function], "getNonScriptedFields": [Function], } } @@ -40,7 +42,7 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` Object { "displayName": "timestamp", "excluded": false, - "format": undefined, + "format": "", "info": Array [], "name": "timestamp", "type": "date", @@ -57,6 +59,7 @@ exports[`IndexedFieldsTable should render normally 1`] = ` editField={[Function]} indexPattern={ Object { + "getFormatterForFieldNoDefault": [Function], "getNonScriptedFields": [Function], } } @@ -65,7 +68,7 @@ exports[`IndexedFieldsTable should render normally 1`] = ` Object { "displayName": "Elastic", "excluded": false, - "format": undefined, + "format": "", "info": Array [], "name": "Elastic", "searchable": true, @@ -74,7 +77,7 @@ exports[`IndexedFieldsTable should render normally 1`] = ` Object { "displayName": "timestamp", "excluded": false, - "format": undefined, + "format": "", "info": Array [], "name": "timestamp", "type": "date", @@ -82,7 +85,7 @@ exports[`IndexedFieldsTable should render normally 1`] = ` Object { "displayName": "conflictingField", "excluded": false, - "format": undefined, + "format": "", "info": Array [], "name": "conflictingField", "type": "conflict", diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap index 2d51b1722cfb2..2548e7e829789 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -143,7 +143,7 @@ exports[`Table should render normally 1`] = ` exports[`Table should render the boolean template (false) 1`] = ``; exports[`Table should render the boolean template (true) 1`] = ` -
fields, + getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), } as unknown) as IndexPattern; const mockFieldToIndexPatternField = (spec: Record) => { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 7be420e2af50d..92f0c4576e931 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -21,7 +21,6 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; import { IndexPatternField, IndexPattern, IFieldType } from '../../../../../../plugins/data/public'; import { Table } from './components/table'; -import { getFieldFormat } from './lib'; import { IndexedFieldItem } from './types'; interface IndexedFieldsTableProps { @@ -73,7 +72,7 @@ export class IndexedFieldsTable extends Component< return { ...field.spec, displayName: field.displayName, - format: getFieldFormat(indexPattern, field.name), + format: indexPattern.getFormatterForFieldNoDefault(field.name)?.type?.title || '', excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts deleted file mode 100644 index 2786df641fdb2..0000000000000 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts +++ /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 { IIndexPattern } from '../../../../../../data/public'; -import { getFieldFormat } from './get_field_format'; - -const indexPattern = ({ - fieldFormatMap: { - Elastic: { - type: { - title: 'string', - }, - }, - }, -} as unknown) as IIndexPattern; - -describe('getFieldFormat', () => { - test('should handle no arguments', () => { - expect(getFieldFormat()).toEqual(''); - }); - - test('should handle no field name', () => { - expect(getFieldFormat(indexPattern)).toEqual(''); - }); - - test('should handle empty name', () => { - expect(getFieldFormat(indexPattern, '')).toEqual(''); - }); - - test('should handle undefined field name', () => { - expect(getFieldFormat(indexPattern, 'none')).toEqual(undefined); - }); - - test('should retrieve field format', () => { - expect(getFieldFormat(indexPattern, 'Elastic')).toEqual('string'); - }); -}); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index 3f4190eed9170..1e8fb6f9492fe 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -30,6 +30,7 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` "getByName": [Function], }, "getFormatterForField": [Function], + "getFormatterForFieldNoDefault": [Function], } } isVisible={false} @@ -265,6 +266,7 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = ` "getByName": [Function], }, "getFormatterForField": [Function], + "getFormatterForFieldNoDefault": [Function], } } isVisible={false} @@ -499,6 +501,7 @@ exports[`FieldEditor should show conflict field warning 1`] = ` "getByName": [Function], }, "getFormatterForField": [Function], + "getFormatterForFieldNoDefault": [Function], } } isVisible={false} @@ -762,6 +765,7 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` "getByName": [Function], }, "getFormatterForField": [Function], + "getFormatterForFieldNoDefault": [Function], } } isVisible={false} @@ -1077,6 +1081,7 @@ exports[`FieldEditor should show multiple type field warning with a table contai "getByName": [Function], }, "getFormatterForField": [Function], + "getFormatterForFieldNoDefault": [Function], } } isVisible={false} diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index 13be0353e1640..b627dbe0576ee 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -54,7 +54,7 @@ exports[`UrlFormatEditor should render normally 1`] = ` -