diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9b8fee1bd5612..c398316e634b9 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -34,10 +34,12 @@
/src/plugins/vis_types/vislib/ @elastic/kibana-vis-editors
/src/plugins/vis_types/xy/ @elastic/kibana-vis-editors
/src/plugins/vis_types/pie/ @elastic/kibana-vis-editors
+/src/plugins/vis_types/heatmap/ @elastic/kibana-vis-editors
/src/plugins/visualize/ @elastic/kibana-vis-editors
/src/plugins/visualizations/ @elastic/kibana-vis-editors
/src/plugins/chart_expressions/expression_tagcloud/ @elastic/kibana-vis-editors
/src/plugins/chart_expressions/expression_metric/ @elastic/kibana-vis-editors
+/src/plugins/chart_expressions/expression_heatmap/ @elastic/kibana-vis-editors
/src/plugins/url_forwarding/ @elastic/kibana-vis-editors
/packages/kbn-tinymath/ @elastic/kibana-vis-editors
/x-pack/test/functional/apps/lens @elastic/kibana-vis-editors
diff --git a/.i18nrc.json b/.i18nrc.json
index 80dbfee949a6c..9485f5b9b84e7 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -28,6 +28,7 @@
"expressionRepeatImage": "src/plugins/expression_repeat_image",
"expressionRevealImage": "src/plugins/expression_reveal_image",
"expressionShape": "src/plugins/expression_shape",
+ "expressionHeatmap": "src/plugins/chart_expressions/expression_heatmap",
"expressionTagcloud": "src/plugins/chart_expressions/expression_tagcloud",
"expressionMetricVis": "src/plugins/chart_expressions/expression_metric",
"inputControl": "src/plugins/input_control_vis",
@@ -69,6 +70,7 @@
"visTypeVislib": "src/plugins/vis_types/vislib",
"visTypeXy": "src/plugins/vis_types/xy",
"visTypePie": "src/plugins/vis_types/pie",
+ "visTypeHeatmap": "src/plugins/vis_types/heatmap",
"visualizations": "src/plugins/visualizations",
"visualize": "src/plugins/visualize",
"apmOss": "src/plugins/apm_oss",
diff --git a/config/kibana.yml b/config/kibana.yml
index f6f85f057172c..aedea8ce83bfb 100644
--- a/config/kibana.yml
+++ b/config/kibana.yml
@@ -99,7 +99,7 @@
# Logs queries sent to Elasticsearch.
#logging.loggers:
-# - name: elasticsearch.queries
+# - name: elasticsearch.query
# level: debug
# Logs http responses.
diff --git a/dev_docs/tutorials/data/search.mdx b/dev_docs/tutorials/data/search.mdx
index 425736ddb03bb..0787c44b632ec 100644
--- a/dev_docs/tutorials/data/search.mdx
+++ b/dev_docs/tutorials/data/search.mdx
@@ -129,6 +129,12 @@ setTimeout(() => {
}, 1000);
```
+
+ Users might no longer be interested in search results. For example, they might start a new search
+ or leave your app without waiting for the results. You should handle such cases by using
+ `AbortController` with search API.
+
+
#### Search strategies
By default, the search service uses the DSL query and aggregation syntax and returns the response from Elasticsearch as is. It also provides several additional basic strategies, such as Async DSL (`x-pack` default) and EQL.
diff --git a/docs/api/alerting.asciidoc b/docs/api/alerting.asciidoc
index ad2d358d17ba0..931165ce5f485 100644
--- a/docs/api/alerting.asciidoc
+++ b/docs/api/alerting.asciidoc
@@ -1,7 +1,7 @@
[[alerting-apis]]
== Alerting APIs
-The following APIs are available for {kib} alerting.
+The following APIs are available for Alerting.
* <> to create a rule
diff --git a/docs/api/alerting/list_rule_types.asciidoc b/docs/api/alerting/list_rule_types.asciidoc
index 21ace9f3105c0..e7b433064eec9 100644
--- a/docs/api/alerting/list_rule_types.asciidoc
+++ b/docs/api/alerting/list_rule_types.asciidoc
@@ -4,7 +4,7 @@
List rule types
++++
-Retrieve a list of alerting rule types that the user is authorized to access.
+Retrieve a list of rule types that the user is authorized to access.
Each rule type includes a list of consumer features. Within these features, users are authorized to perform either `read` or `all` operations on rules of that type. This helps determine which rule types users can read, but not create or modify.
diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc
index 8bf1b38920141..a7e2a93e0944e 100644
--- a/docs/apm/api.asciidoc
+++ b/docs/apm/api.asciidoc
@@ -10,7 +10,6 @@ Some APM app features are provided via a REST API:
* <>
* <>
-* <>
* <>
[float]
@@ -71,6 +70,13 @@ curl -X POST \
}'
----
+[float]
+[[kibana-api]]
+=== Kibana API
+
+In addition to the APM specific API endpoints, Kibana provides its own <>
+which you can use to automate certain aspects of configuring and deploying Kibana.
+
////
*******************************************************
*******************************************************
@@ -474,90 +480,6 @@ curl -X POST \
*******************************************************
////
-[[kibana-api]]
-=== Kibana API
-
-In addition to the APM specific API endpoints, Kibana provides its own <>
-which you can use to automate certain aspects of configuring and deploying Kibana.
-An example is below.
-
-[[api-create-apm-index-pattern]]
-==== Customize the APM index pattern
-
-Use Kibana's <> to update the default APM index pattern on the fly.
-
-The following example sets the default APM app index pattern to `some-other-pattern-*`:
-
-[source,sh]
-----
-curl -X PUT "localhost:5601/api/saved_objects/index-pattern/apm_static_index_pattern_id" \ <1>
--H 'Content-Type: application/json' \
--H 'kbn-xsrf: true' \
--H 'Authorization: Basic ${YOUR_AUTH_TOKEN}' \
--d' {
- "attributes": {
- "title": "some-other-pattern-*", <2>
- }
- }'
-----
-<1> `apm_static_index_pattern_id` is the internal, hard-coded ID of the APM index pattern.
-This value should not be changed
-<2> Your custom index pattern matcher.
-
-The API returns the following:
-
-[source,json]
-----
-{
- "id":"apm_static_index_pattern_id",
- "type":"index-pattern",
- "updated_at":"2020-07-06T22:55:59.555Z",
- "version":"WzYsMV0=",
- "attributes":{
- "title":"some-other-pattern-*"
- }
-}
-----
-
-To view the new APM app index pattern, use the <>:
-
-[source,sh]
-----
-curl -X GET "localhost:5601/api/saved_objects/index-pattern/apm_static_index_pattern_id" \ <1>
--H 'kbn-xsrf: true' \
--H 'Authorization: Basic ${YOUR_AUTH_TOKEN}'
-----
-<1> `apm_static_index_pattern_id` is the internal, hard-coded ID of the APM index pattern.
-
-The API returns the following:
-
-[source,json]
-----
-{
- "id":"apm_static_index_pattern_id",
- "type":"index-pattern",
- "updated_at":"2020-07-06T22:55:59.555Z",
- "version":"WzYsMV0=",
- "attributes":{...}
- "fieldFormatMap":"{...}
- "fields":"[{...},{...},...]
- "sourceFilters":"[{\"value\":\"sourcemap.sourcemap\"}]",
- "timeFieldName":"@timestamp",
- "title":"some-other-pattern-*"
- },
- ...
-}
-----
-
-// More examples will go here
-
-More information on Kibana's API is available in <>.
-
-////
-*******************************************************
-*******************************************************
-////
-
[role="xpack"]
[[rum-sourcemap-api]]
=== RUM source map API
diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc
index 42016ac08bfc7..d8ce1fafc783c 100644
--- a/docs/apm/apm-alerts.asciidoc
+++ b/docs/apm/apm-alerts.asciidoc
@@ -107,7 +107,7 @@ From this page, you can disable, mute, and delete APM alerts.
[[apm-alert-more-info]]
=== More information
-See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more information.
+See {kibana-ref}/alerting-getting-started.html[Alerting] for more information.
NOTE: If you are using an **on-premise** Elastic Stack deployment with security,
communication between Elasticsearch and Kibana must have TLS configured.
diff --git a/docs/apm/images/apm-setup.png b/docs/apm/images/apm-setup.png
index 3410ebf69d846..8aadd8911c6e8 100644
Binary files a/docs/apm/images/apm-setup.png and b/docs/apm/images/apm-setup.png differ
diff --git a/docs/apm/set-up.asciidoc b/docs/apm/set-up.asciidoc
index 3cbe45ec913b7..481ac52d8ffdc 100644
--- a/docs/apm/set-up.asciidoc
+++ b/docs/apm/set-up.asciidoc
@@ -8,28 +8,13 @@
APM is available via the navigation sidebar in {Kib}.
If you have not already installed and configured Elastic APM,
-the *Add data* page will get you started.
+follow the three steps on the *Add data* page to get started:
-[role="screenshot"]
-image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana]
-
-[float]
-[[apm-configure-index-pattern]]
-=== Load the index pattern
-
-Index patterns tell {kib} which {es} indices you want to explore.
-An APM index pattern is necessary for certain features in the APM app, like the query bar.
-To set up the correct index pattern, on the *Add data* page, click *Load Kibana objects*.
+. Start APM Server
+. Add APM agents
+. Load Kibana objects
[role="screenshot"]
-image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana]
-
-TIP: To use a custom index pattern,
-adjust Kibana's <> or use the <>.
-
-[float]
-[[apm-getting-started-next]]
-=== Next steps
+image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana]
-No further configuration in the APM app is required.
-Install an APM Agent library in your service to begin visualizing and analyzing your data!
+That's it! You're now ready to explore your data.
diff --git a/docs/concepts/data-views.asciidoc b/docs/concepts/data-views.asciidoc
index 954581faa2460..870b923f20cf4 100644
--- a/docs/concepts/data-views.asciidoc
+++ b/docs/concepts/data-views.asciidoc
@@ -87,11 +87,12 @@ For an example, refer to <:
+:
```
To query {ls} indices across two {es} clusters
diff --git a/docs/developer/contributing/development-accessibility-tests.asciidoc b/docs/developer/contributing/development-accessibility-tests.asciidoc
index 584d779bc7de6..2fe2682a3e365 100644
--- a/docs/developer/contributing/development-accessibility-tests.asciidoc
+++ b/docs/developer/contributing/development-accessibility-tests.asciidoc
@@ -90,7 +90,7 @@ Failures can seem confusing if you've never seen one before. Here is a breakdown
[aria-hidden-focus]: Ensures aria-hidden elements do not contain focusable elements
Help: https://dequeuniversity.com/rules/axe/3.5/aria-hidden-focus?application=axeAPI
Elements:
- - #example
+ - Submit
at Accessibility.testAxeReport (test/accessibility/services/a11y/a11y.ts:90:15)
at Accessibility.testAppSnapshot (test/accessibility/services/a11y/a11y.ts:58:18)
at process._tickCallback (internal/process/next_tick.js:68:7)
@@ -100,5 +100,5 @@ Failures can seem confusing if you've never seen one before. Here is a breakdown
* "Dashboard" and "create dashboard button" are the names of the test suite and specific test that failed.
* Always in brackets, "[aria-hidden-focus]" is the name of the axe rule that failed, followed by a short description.
* "Help: " links to the axe documentation for that rule, including severity, remediation tips, and good and bad code examples.
-* "Elements:" points to where in the DOM the failure originated (using CSS selector syntax). In this example, the problem came from an element with the ID `example`. If the selector is too complicated to find the source of the problem, use the browser plugins mentioned earlier to locate it. If you have a general idea where the issue is coming from, you can also try adding unique IDs to the page to narrow down the location.
+* "Elements:" points to where in the DOM the failure originated (using HTML syntax). In this example, the problem came from a span with the `aria-hidden="true"` attribute and a nested `` tag. If the selector is too complicated to find the source of the problem, use the browser plugins mentioned earlier to locate it. If you have a general idea where the issue is coming from, you can also try adding unique IDs to the page to narrow down the location.
* The stack trace points to the internals of axe. The stack trace is there in case the test failure is a bug in axe and not in your code, although this is unlikely.
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 2f0be0c39a3b8..e997c0bc68cde 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -94,6 +94,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a
|Expression Error plugin adds an error renderer to the expression plugin. The renderer will display the error image.
+|{kib-repo}blob/{branch}/src/plugins/chart_expressions/expression_heatmap[expressionHeatmap]
+|WARNING: Missing README.
+
+
|{kib-repo}blob/{branch}/src/plugins/expression_image/README.md[expressionImage]
|Expression Image plugin adds an image renderer to the expression plugin. The renderer will display the given image.
@@ -274,6 +278,10 @@ It acts as a container for a particular visualization and options tabs. Contains
The plugin exposes the static DefaultEditorController class to consume.
+|{kib-repo}blob/{branch}/src/plugins/vis_types/heatmap[visTypeHeatmap]
+|WARNING: Missing README.
+
+
|{kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown]
|The markdown visualization that can be used to place text panels on dashboards.
diff --git a/docs/index-extra-title-page.html b/docs/index-extra-title-page.html
index ff1c879c0f409..b70e3a985f22b 100644
--- a/docs/index-extra-title-page.html
+++ b/docs/index-extra-title-page.html
@@ -63,7 +63,7 @@
>
- Create a data view
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 581f13cbc2992..5906f2dd5008f 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -521,7 +521,10 @@ Enables the legacy time axis for charts in Lens, Discover, Visualize and TSVB
The maximum number of buckets a datasource can return. High numbers can have a negative impact on your browser rendering performance.
[[visualization-visualize-pieChartslibrary]]`visualization:visualize:legacyPieChartsLibrary`::
-The visualize editor uses new pie charts with improved performance, color palettes, label positioning, and more. Enable this option if you prefer to use to the legacy charts library.
+The visualize editor uses new pie charts with improved performance, color palettes, label positioning, and more. Enable this option if you prefer to use the legacy charts library.
+
+[[visualization-visualize-heatmapChartslibrary]]`visualization:visualize:legacyHeatmapChartsLibrary`::
+Disable this option if you prefer to use the new heatmap charts with improved performance, legend settings, and more..
[[visualize-enablelabs]]`visualize:enableLabs`::
Enables users to create, view, and edit experimental visualizations. When disabled,
diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc
index 8b77bb7555027..e827b032fb5bf 100644
--- a/docs/redirects.asciidoc
+++ b/docs/redirects.asciidoc
@@ -15,6 +15,10 @@ Refer to {ref}/snapshot-restore.html[Snapshot and Restore].
== Tutorial: Snapshot and Restore
Refer to {ref}/snapshot-restore.html[Snapshot and Restore].
+[role="exclude",id="configuring-tls-communication"]
+== Encrypt communications in {kib}
+Refer to <>.
+
[role="exclude",id="configuring-tls"]
== Encrypt TLS communications in {kib}
Refer to {ref}/security-basic-setup-https.html#encrypt-kibana-http[Encrypt HTTP client communications for {kib}].
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index 38a73ec92313f..66e23686e14e0 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -5,7 +5,7 @@
Alerting and action settings
++++
-Alerts and actions are enabled by default in {kib}, but require you configure the following in order to use them:
+Alerting and actions are enabled by default in {kib}, but require you to configure the following:
. <>.
. <>.
diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc
index d343aa12ec806..77a250a14f929 100644
--- a/docs/settings/apm-settings.asciidoc
+++ b/docs/settings/apm-settings.asciidoc
@@ -101,6 +101,8 @@ Changing these settings may disable features of the APM App.
| `xpack.apm.indices.sourcemap` {ess-icon}
| Matcher for all source map indices. Defaults to `apm-*`.
+| `xpack.apm.autocreateApmIndexPattern` {ess-icon}
+ | Set to `false` to disable the automatic creation of the APM index pattern when the APM app is opened. Defaults to `true`.
|===
// end::general-apm-settings[]
\ No newline at end of file
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index 9e2f981e92f92..14c1002fd348a 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -21,7 +21,7 @@ configuration using `${MY_ENV_VAR}` syntax.
|===
| `console.ui.enabled:`
-Toggling this causes the server to regenerate assets on the next startup,
+| Toggling this causes the server to regenerate assets on the next startup,
which may cause a delay before pages start being served.
Set to `false` to disable Console. *Default: `true`*
diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc
index 80ad1bb630f53..584d45dc088fd 100644
--- a/docs/user/alerting/alerting-getting-started.asciidoc
+++ b/docs/user/alerting/alerting-getting-started.asciidoc
@@ -125,18 +125,18 @@ image::images/rule-concepts-summary.svg[Rules, connectors, alerts and actions wo
[[alerting-concepts-differences]]
== Differences from Watcher
-{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems.
+Alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems.
This section will clarify some of the important differences in the function and intent of the two systems.
-Functionally, {kib} alerting differs in that:
+Functionally, Alerting differs in that:
* Scheduled checks are run on {kib} instead of {es}
* {kib} <> through *rule types*, whereas watches provide low-level control over inputs, conditions, and transformations.
* {kib} rules track and persist the state of each detected condition through *alerts*. This makes it possible to mute and throttle individual alerts, and detect changes in state such as resolution.
-* Actions are linked to *alerts* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire rule.
+* Actions are linked to *alerts* in Alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire rule.
-At a higher level, {kib} alerting allows rich integrations across use cases like <>, <>, <>, and <>.
+At a higher level, Alerting allows rich integrations across use cases like <>, <>, <>, and <>.
Pre-packaged *rule types* simplify setup and hide the details of complex, domain-specific detections, while providing a consistent interface across {kib}.
--
diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc
index 9aae3a27cf373..6f8caedde3e18 100644
--- a/docs/user/alerting/alerting-setup.asciidoc
+++ b/docs/user/alerting/alerting-setup.asciidoc
@@ -5,7 +5,7 @@
Set up
++++
-The Alerting feature is automatically enabled in {kib}, but might require some additional configuration.
+Alerting is automatically enabled in {kib}, but might require some additional configuration.
[float]
[[alerting-prerequisites]]
@@ -17,9 +17,9 @@ If you are using an *on-premises* Elastic Stack deployment:
If you are using an *on-premises* Elastic Stack deployment with <>:
-* If you are unable to access Alerting, ensure that you have not {ref}/security-settings.html#api-key-service-settings[explicitly disabled API keys].
+* If you are unable to access {kib} Alerting, ensure that you have not {ref}/security-settings.html#api-key-service-settings[explicitly disabled API keys].
-The Alerting framework uses queries that require the `search.allow_expensive_queries` setting to be `true`. See the scripts {ref}/query-dsl-script-query.html#_allow_expensive_queries_4[documentation].
+The alerting framework uses queries that require the `search.allow_expensive_queries` setting to be `true`. See the scripts {ref}/query-dsl-script-query.html#_allow_expensive_queries_4[documentation].
[float]
[[alerting-setup-production]]
@@ -27,7 +27,7 @@ The Alerting framework uses queries that require the `search.allow_expensive_que
When relying on alerting and actions as mission critical services, make sure you follow the <>.
-See <> for more information on the scalability of {kib} alerting.
+See <> for more information on the scalability of Alerting.
[float]
[[alerting-security]]
diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc
index e45a77d48da9c..74a32b94975ad 100644
--- a/docs/user/alerting/alerting-troubleshooting.asciidoc
+++ b/docs/user/alerting/alerting-troubleshooting.asciidoc
@@ -5,7 +5,7 @@
Troubleshooting
++++
-The Alerting framework provides many options for diagnosing problems with Rules and Connectors.
+Alerting provides many options for diagnosing problems with Rules and Connectors.
[float]
[[alerting-kibana-log]]
diff --git a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc
index 408b18143f27f..52153cfdde747 100644
--- a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc
+++ b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc
@@ -35,7 +35,7 @@ Actions run long after the status of a rule changes, sending a notification of t
*Solution*
Rules and actions run as background tasks by each {kib} instance at a default rate of ten tasks every three seconds.
-When diagnosing issues related to Alerting, focus on the tasks that begin with `alerting:` and `actions:`.
+When diagnosing issues related to alerting, focus on the tasks that begin with `alerting:` and `actions:`.
Alerting tasks always begin with `alerting:`. For example, the `alerting:.index-threshold` tasks back the <>.
Action tasks always begin with `actions:`. For example, the `actions:.index` tasks back the <>.
diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc
index f6deaed7fa3b9..eedc19a189887 100644
--- a/docs/user/monitoring/kibana-alerts.asciidoc
+++ b/docs/user/monitoring/kibana-alerts.asciidoc
@@ -3,7 +3,7 @@
= {kib} alerts
The {stack} {monitor-features} provide
-<> out-of-the box to notify you
+<> out-of-the box to notify you
of potential issues in the {stack}. These rules are preconfigured based on the
best practices recommended by Elastic. However, you can tailor them to meet your
specific needs.
diff --git a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc
index 98201087b9aae..09eb304646e96 100644
--- a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc
+++ b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc
@@ -975,7 +975,7 @@ server log [12:41:33.672] [info][plugins][taskManager][taskManager] TaskManager
--------------------------------------------------
If you see that message and no other errors that relate to Task Manager, it’s most likely that Task Manager is running fine and has simply not had the chance to pick the task up yet.
-If, on the other hand, the runAt is severely overdue, then it’s worth looking for other Task Manager or Alerting related errors, as something else may have gone wrong.
+If, on the other hand, the runAt is severely overdue, then it’s worth looking for other Task Manager or alerting-related errors, as something else may have gone wrong.
It’s worth looking at the status field, as it might have failed, which would explain why it hasn’t been picked up or it might be running which means the task might simply be a very long running one.
[float]
diff --git a/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc
index 1bc1f579ecbc7..d0a0cb49c36bd 100644
--- a/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc
+++ b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc
@@ -33,8 +33,7 @@ configures {kib} to authenticate with {es} using a
needs to map the client certificate's distinguished name (DN) to the appropriate
`kibana_system` role.
-NOTE: Using a PKI realm is a gold feature. For a comparison of the Elastic
-license levels, see https://www.elastic.co/subscriptions[the subscription page].
+NOTE: Using a PKI realm is a https://www.elastic.co/subscriptions[subscription feature].
[discrete]
==== Configure {kib} and {es} to use mutual TLS authentication
diff --git a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc
index afe76dcabb844..dd913a5bb28d8 100644
--- a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc
+++ b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc
@@ -11,9 +11,9 @@ This guide introduces you to three of {kib}'s security features: spaces, roles,
[float]
=== Spaces
-Do you have multiple teams or tenants using {kib}? Do you want a “playground” to experiment with new visualizations or alerts? If so, then <> can help.
+Do you have multiple teams or tenants using {kib}? Do you want a “playground” to experiment with new visualizations or rules? If so, then <> can help.
-Think of a space as another instance of {kib}. A space allows you to organize your <>, <>, <>, and much more into their own categories. For example, you might have a Marketing space for your marketeers to track the results of their campaigns, and an Engineering space for your developers to {apm-guide-ref}/apm-overview.html[monitor application performance].
+Think of a space as another instance of {kib}. A space allows you to organize your <>, <>, <>, and much more into their own categories. For example, you might have a Marketing space for your marketeers to track the results of their campaigns, and an Engineering space for your developers to {apm-guide-ref}/apm-overview.html[monitor application performance].
The assets you create in one space are isolated from other spaces, so when you enter a space, you only see the assets that belong to that space.
diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx
index 1f8cda9443fa7..eeceab569d3b3 100644
--- a/examples/search_examples/public/search/app.tsx
+++ b/examples/search_examples/public/search/app.tsx
@@ -47,6 +47,7 @@ import {
isErrorResponse,
} from '../../../../src/plugins/data/public';
import { IMyStrategyResponse } from '../../common/types';
+import { AbortError } from '../../../../src/plugins/kibana_utils/common';
interface SearchExamplesAppDeps {
notifications: CoreStart['notifications'];
@@ -102,6 +103,8 @@ export const SearchExamplesApp = ({
IndexPatternField | null | undefined
>();
const [request, setRequest] = useState>({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [currentAbortController, setAbortController] = useState();
const [rawResponse, setRawResponse] = useState>({});
const [selectedTab, setSelectedTab] = useState(0);
@@ -187,16 +190,23 @@ export const SearchExamplesApp = ({
...(strategy ? { get_cool: getCool } : {}),
};
+ const abortController = new AbortController();
+ setAbortController(abortController);
+
// Submit the search request using the `data.search` service.
setRequest(req.params.body);
- const searchSubscription$ = data.search
+ setIsLoading(true);
+
+ data.search
.search(req, {
strategy,
sessionId,
+ abortSignal: abortController.signal,
})
.subscribe({
next: (res) => {
if (isCompleteResponse(res)) {
+ setIsLoading(false);
setResponse(res);
const avgResult: number | undefined = res.rawResponse.aggregations
? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
@@ -226,7 +236,6 @@ export const SearchExamplesApp = ({
toastLifeTimeMs: 300000,
}
);
- searchSubscription$.unsubscribe();
if (res.warning) {
notifications.toasts.addWarning({
title: 'Warning',
@@ -236,14 +245,20 @@ export const SearchExamplesApp = ({
} else if (isErrorResponse(res)) {
// TODO: Make response error status clearer
notifications.toasts.addDanger('An error has occurred');
- searchSubscription$.unsubscribe();
}
},
error: (e) => {
- notifications.toasts.addDanger({
- title: 'Failed to run search',
- text: e.message,
- });
+ setIsLoading(false);
+ if (e instanceof AbortError) {
+ notifications.toasts.addWarning({
+ title: e.message,
+ });
+ } else {
+ notifications.toasts.addDanger({
+ title: 'Failed to run search',
+ text: e.message,
+ });
+ }
},
});
};
@@ -286,7 +301,12 @@ export const SearchExamplesApp = ({
}
setRequest(searchSource.getSearchRequestBody());
- const { rawResponse: res } = await searchSource.fetch$().toPromise();
+ const abortController = new AbortController();
+ setAbortController(abortController);
+ setIsLoading(true);
+ const { rawResponse: res } = await searchSource
+ .fetch$({ abortSignal: abortController.signal })
+ .toPromise();
setRawResponse(res);
const message = Searched {res.hits.total} documents. ;
@@ -301,7 +321,18 @@ export const SearchExamplesApp = ({
);
} catch (e) {
setRawResponse(e.body);
- notifications.toasts.addWarning(`An error has occurred: ${e.message}`);
+ if (e instanceof AbortError) {
+ notifications.toasts.addWarning({
+ title: e.message,
+ });
+ } else {
+ notifications.toasts.addDanger({
+ title: 'Failed to run search',
+ text: e.message,
+ });
+ }
+ } finally {
+ setIsLoading(false);
}
};
@@ -329,32 +360,44 @@ export const SearchExamplesApp = ({
},
};
+ const abortController = new AbortController();
+ setAbortController(abortController);
+
// Submit the search request using the `data.search` service.
setRequest(req.params);
- const searchSubscription$ = data.search
+ setIsLoading(true);
+ data.search
.search(req, {
strategy: 'fibonacciStrategy',
+ abortSignal: abortController.signal,
})
.subscribe({
next: (res) => {
setResponse(res);
if (isCompleteResponse(res)) {
+ setIsLoading(false);
notifications.toasts.addSuccess({
title: 'Query result',
text: 'Query finished',
});
- searchSubscription$.unsubscribe();
} else if (isErrorResponse(res)) {
+ setIsLoading(false);
// TODO: Make response error status clearer
notifications.toasts.addWarning('An error has occurred');
- searchSubscription$.unsubscribe();
}
},
error: (e) => {
- notifications.toasts.addDanger({
- title: 'Failed to run search',
- text: e.message,
- });
+ setIsLoading(false);
+ if (e instanceof AbortError) {
+ notifications.toasts.addWarning({
+ title: e.message,
+ });
+ } else {
+ notifications.toasts.addDanger({
+ title: 'Failed to run search',
+ text: e.message,
+ });
+ }
},
});
};
@@ -365,17 +408,32 @@ export const SearchExamplesApp = ({
const onServerClickHandler = async () => {
if (!indexPattern || !selectedNumericField) return;
+ const abortController = new AbortController();
+ setAbortController(abortController);
+ setIsLoading(true);
try {
const res = await http.get(SERVER_SEARCH_ROUTE_PATH, {
query: {
index: indexPattern.title,
field: selectedNumericField!.name,
},
+ signal: abortController.signal,
});
notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`);
} catch (e) {
- notifications.toasts.addDanger('Failed to run search');
+ if (e?.name === 'AbortError') {
+ notifications.toasts.addWarning({
+ title: e.message,
+ });
+ } else {
+ notifications.toasts.addDanger({
+ title: 'Failed to run search',
+ text: e.message,
+ });
+ }
+ } finally {
+ setIsLoading(false);
}
};
@@ -721,6 +779,11 @@ export const SearchExamplesApp = ({
strategy. This request does not take the configuration of{' '}
TopNavMenu into account, but you could pass those down to the
server as well.
+
+ When executing search on the server, make sure to cancel the search in case user
+ cancels corresponding network request. This could happen in case user re-runs a
+ query or leaves the page without waiting for the result. Cancellation API is similar
+ on client and server and use `AbortController`.
setSelectedTab(reqTabs.indexOf(tab))}
/>
+
+ {currentAbortController && isLoading && (
+ currentAbortController?.abort()}>
+
+
+ )}
diff --git a/examples/search_examples/server/routes/server_search_route.ts b/examples/search_examples/server/routes/server_search_route.ts
index 258587610a207..0d1302233a39c 100644
--- a/examples/search_examples/server/routes/server_search_route.ts
+++ b/examples/search_examples/server/routes/server_search_route.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { Observable } from 'rxjs';
import { IEsSearchRequest } from 'src/plugins/data/server';
import { schema } from '@kbn/config-schema';
import { IEsSearchResponse } from 'src/plugins/data/common';
@@ -26,36 +27,51 @@ export function registerServerSearchRoute(router: IRouter {
const { index, field } = request.query;
- // Run a synchronous search server side, by enforcing a high keepalive and waiting for completion.
- // If you wish to run the search with polling (in basic+), you'd have to poll on the search API.
- // Please reach out to the @app-arch-team if you need this to be implemented.
- const res = await context
- .search!.search(
- {
- params: {
- index,
- body: {
- aggs: {
- '1': {
- avg: {
- field,
+
+ // User may abort the request without waiting for the results
+ // we need to handle this scenario by aborting underlying server requests
+ const abortSignal = getRequestAbortedSignal(request.events.aborted$);
+
+ try {
+ const res = await context
+ .search!.search(
+ {
+ params: {
+ index,
+ body: {
+ aggs: {
+ '1': {
+ avg: {
+ field,
+ },
},
},
},
},
- waitForCompletionTimeout: '5m',
- keepAlive: '5m',
- },
- } as IEsSearchRequest,
- {}
- )
- .toPromise();
+ } as IEsSearchRequest,
+ { abortSignal }
+ )
+ .toPromise();
- return response.ok({
- body: {
- aggs: (res as IEsSearchResponse).rawResponse.aggregations,
- },
- });
+ return response.ok({
+ body: {
+ aggs: (res as IEsSearchResponse).rawResponse.aggregations,
+ },
+ });
+ } catch (e) {
+ return response.customError({
+ statusCode: e.statusCode ?? 500,
+ body: {
+ message: e.message,
+ },
+ });
+ }
}
);
}
+
+function getRequestAbortedSignal(aborted$: Observable): AbortSignal {
+ const controller = new AbortController();
+ aborted$.subscribe(() => controller.abort());
+ return controller.signal;
+}
diff --git a/package.json b/package.json
index b0c7e4659a559..cbe8c64a67368 100644
--- a/package.json
+++ b/package.json
@@ -557,6 +557,8 @@
"@types/json5": "^0.0.30",
"@types/kbn__ace": "link:bazel-bin/packages/kbn-ace/npm_module_types",
"@types/kbn__alerts": "link:bazel-bin/packages/kbn-alerts/npm_module_types",
+ "@types/kbn__analytics": "link:bazel-bin/packages/kbn-analytics/npm_module_types",
+ "@types/kbn__apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module_types",
"@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types",
"@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types",
"@types/license-checker": "15.0.0",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index c9a0f6a759b2a..b6aa948de0fa4 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -79,6 +79,8 @@ filegroup(
"//packages/elastic-datemath:build_types",
"//packages/kbn-ace:build_types",
"//packages/kbn-alerts:build_types",
+ "//packages/kbn-analytics:build_types",
+ "//packages/kbn-apm-utils:build_types",
"//packages/kbn-i18n:build_types",
"//packages/kbn-i18n-react:build_types",
],
diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel
index cc65746e890ce..94e65b2e35ba3 100644
--- a/packages/kbn-analytics/BUILD.bazel
+++ b/packages/kbn-analytics/BUILD.bazel
@@ -1,9 +1,10 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
-load("//src/dev/bazel:index.bzl", "jsts_transpiler")
+load("@npm//@bazel/typescript:index.bzl", "ts_config")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
+load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_BASE_NAME = "kbn-analytics"
PKG_REQUIRE_NAME = "@kbn/analytics"
+TYPES_PKG_REQUIRE_NAME = "@types/kbn__analytics"
SOURCE_FILES = glob(
[
@@ -81,7 +82,7 @@ ts_project(
js_library(
name = PKG_BASE_NAME,
srcs = NPM_MODULE_EXTRA_FILES,
- deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"],
+ deps = RUNTIME_DEPS + [":target_node", ":target_web"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
@@ -100,3 +101,20 @@ filegroup(
],
visibility = ["//visibility:public"],
)
+
+pkg_npm_types(
+ name = "npm_module_types",
+ srcs = SRCS,
+ deps = [":tsc_types"],
+ package_name = TYPES_PKG_REQUIRE_NAME,
+ tsconfig = ":tsconfig",
+ visibility = ["//visibility:public"],
+)
+
+filegroup(
+ name = "build_types",
+ srcs = [
+ ":npm_module_types",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json
index 177c0eb815760..c3b30dcf79431 100644
--- a/packages/kbn-analytics/package.json
+++ b/packages/kbn-analytics/package.json
@@ -4,7 +4,6 @@
"version": "1.0.0",
"description": "Kibana Analytics tool",
"main": "target_node/index.js",
- "types": "target_types/index.d.ts",
"browser": "target_web/index.js",
"author": "Ahmad Bamieh ",
"license": "SSPL-1.0 OR Elastic License 2.0"
diff --git a/packages/kbn-apm-utils/BUILD.bazel b/packages/kbn-apm-utils/BUILD.bazel
index fdfd593476fe7..c8ad4d1a09625 100644
--- a/packages/kbn-apm-utils/BUILD.bazel
+++ b/packages/kbn-apm-utils/BUILD.bazel
@@ -1,9 +1,10 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
-load("//src/dev/bazel:index.bzl", "jsts_transpiler")
+load("@npm//@bazel/typescript:index.bzl", "ts_config")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
+load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_BASE_NAME = "kbn-apm-utils"
PKG_REQUIRE_NAME = "@kbn/apm-utils"
+TYPES_PKG_REQUIRE_NAME = "@types/kbn__apm-utils"
SOURCE_FILES = glob([
"src/index.ts",
@@ -61,7 +62,7 @@ ts_project(
js_library(
name = PKG_BASE_NAME,
srcs = NPM_MODULE_EXTRA_FILES,
- deps = RUNTIME_DEPS + [":target_node", ":tsc_types"],
+ deps = RUNTIME_DEPS + [":target_node"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
@@ -80,3 +81,20 @@ filegroup(
],
visibility = ["//visibility:public"],
)
+
+pkg_npm_types(
+ name = "npm_module_types",
+ srcs = SRCS,
+ deps = [":tsc_types"],
+ package_name = TYPES_PKG_REQUIRE_NAME,
+ tsconfig = ":tsconfig",
+ visibility = ["//visibility:public"],
+)
+
+filegroup(
+ name = "build_types",
+ srcs = [
+ ":npm_module_types",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-apm-utils/package.json b/packages/kbn-apm-utils/package.json
index 2460bc7c14cce..c57753bc83e50 100644
--- a/packages/kbn-apm-utils/package.json
+++ b/packages/kbn-apm-utils/package.json
@@ -1,7 +1,6 @@
{
"name": "@kbn/apm-utils",
"main": "./target_node/index.js",
- "types": "./target_types/index.d.ts",
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",
"private": true
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index e08b34f70b2a1..41c4d3bdd1b35 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -96,6 +96,7 @@ pageLoadAssetSize:
securitySolution: 273763
customIntegrations: 28810
expressionMetricVis: 23121
+ expressionHeatmap: 27505
visTypeMetric: 23332
bfetch: 22837
kibanaUtils: 79713
@@ -115,3 +116,4 @@ pageLoadAssetSize:
dataViewFieldEditor: 20000
dataViewManagement: 5000
reporting: 57003
+ visTypeHeatmap: 25340
diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel
index b135ae4021400..3da5e0ed9a6ff 100644
--- a/packages/kbn-ui-shared-deps-src/BUILD.bazel
+++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel
@@ -44,7 +44,7 @@ RUNTIME_DEPS = [
TYPES_DEPS = [
"//packages/elastic-datemath:npm_module_types",
"//packages/elastic-safer-lodash-set",
- "//packages/kbn-analytics",
+ "//packages/kbn-analytics:npm_module_types",
"//packages/kbn-babel-preset",
"//packages/kbn-i18n:npm_module_types",
"//packages/kbn-i18n-react:npm_module_types",
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 5bc7691d6a40f..7a599e7c84acf 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -246,6 +246,7 @@ export class DocLinksService {
ilmWaitForSnapshot: `${ELASTICSEARCH_DOCS}ilm-wait-for-snapshot.html`,
indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`,
indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`,
+ dynamicIndexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#dynamic-index-settings`,
indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`,
mapping: `${ELASTICSEARCH_DOCS}mapping.html`,
mappingAnalyzer: `${ELASTICSEARCH_DOCS}analyzer.html`,
diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts
index 298144ca95a02..67d7d702c8df9 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.ts
@@ -250,11 +250,11 @@ const deprecations: ConfigDeprecationProvider = () => [
if (es.logQueries === true) {
addDeprecation({
configPath: `${fromPath}.logQueries`,
- message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers".`,
+ message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.query" context in "logging.loggers".`,
correctiveActions: {
manualSteps: [
`Remove Setting [${fromPath}.logQueries] from your kibana configs`,
- `Set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers".`,
+ `Set the log level to "debug" for the "elasticsearch.query" context in "logging.loggers".`,
],
},
});
diff --git a/src/core/server/http/auth_headers_storage.ts b/src/core/server/http/auth_headers_storage.ts
index cddf84bfa8caf..d991f4298ba11 100644
--- a/src/core/server/http/auth_headers_storage.ts
+++ b/src/core/server/http/auth_headers_storage.ts
@@ -5,8 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
-import { KibanaRequest, ensureRawRequest, LegacyRequest } from './router';
+import { Request } from '@hapi/hapi';
+import { KibanaRequest, ensureRawRequest } from './router';
import { AuthHeaders } from './lifecycle/auth';
/**
@@ -19,8 +19,8 @@ export type GetAuthHeaders = (request: KibanaRequest) => AuthHeaders | undefined
/** @internal */
export class AuthHeadersStorage {
- private authHeadersCache = new WeakMap();
- public set = (request: KibanaRequest | LegacyRequest, headers: AuthHeaders) => {
+ private authHeadersCache = new WeakMap();
+ public set = (request: KibanaRequest | Request, headers: AuthHeaders) => {
this.authHeadersCache.set(ensureRawRequest(request), headers);
};
public get: GetAuthHeaders = (request) => {
diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts
index 2dd49f8fb99cc..e85be12cb398c 100644
--- a/src/core/server/http/auth_state_storage.ts
+++ b/src/core/server/http/auth_state_storage.ts
@@ -5,8 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
-import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router';
+import { Request } from '@hapi/hapi';
+import { ensureRawRequest, KibanaRequest } from './router';
/**
* Status indicating an outcome of the authentication.
@@ -45,12 +45,12 @@ export type IsAuthenticated = (request: KibanaRequest) => boolean;
/** @internal */
export class AuthStateStorage {
- private readonly storage = new WeakMap();
+ private readonly storage = new WeakMap();
constructor(private readonly canBeAuthenticated: () => boolean) {}
- public set = (request: KibanaRequest | LegacyRequest, state: unknown) => {
+ public set = (request: KibanaRequest | Request, state: unknown) => {
this.storage.set(ensureRawRequest(request), state);
};
- public get = (request: KibanaRequest | LegacyRequest) => {
+ public get = (request: KibanaRequest | Request) => {
const key = ensureRawRequest(request);
const state = this.storage.get(key) as T;
const status: AuthStatus = this.storage.has(key)
diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts
index 7de943a479810..e52b627041290 100644
--- a/src/core/server/http/base_path_service.ts
+++ b/src/core/server/http/base_path_service.ts
@@ -7,8 +7,8 @@
*/
import { modifyUrl } from '@kbn/std';
-
-import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router';
+import { Request } from '@hapi/hapi';
+import { ensureRawRequest, KibanaRequest } from './router';
/**
* Access or manipulate the Kibana base path
@@ -16,7 +16,7 @@ import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router';
* @public
*/
export class BasePath {
- private readonly basePathCache = new WeakMap();
+ private readonly basePathCache = new WeakMap();
/**
* returns the server's basePath
diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts
index 5ba8143936563..8acb2972e2676 100644
--- a/src/core/server/http/router/index.ts
+++ b/src/core/server/http/router/index.ts
@@ -23,7 +23,6 @@ export type {
KibanaRequestRouteOptions,
KibanaRouteOptions,
KibanaRequestState,
- LegacyRequest,
} from './request';
export { isSafeMethod, validBodyOutput } from './route';
export type {
diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts
index 94d353e1335b3..e53a30124a420 100644
--- a/src/core/server/http/router/request.ts
+++ b/src/core/server/http/router/request.ts
@@ -77,13 +77,6 @@ export interface KibanaRequestEvents {
completed$: Observable;
}
-/**
- * @deprecated
- * `hapi` request object, supported during migration process only for backward compatibility.
- * @public
- */
-export interface LegacyRequest extends Request {} // eslint-disable-line @typescript-eslint/no-empty-interface
-
/**
* Kibana specific abstraction for an incoming request.
* @public
@@ -312,7 +305,7 @@ export class KibanaRequest<
* Returns underlying Hapi Request
* @internal
*/
-export const ensureRawRequest = (request: KibanaRequest | LegacyRequest) =>
+export const ensureRawRequest = (request: KibanaRequest | Request) =>
isKibanaRequest(request) ? request[requestSymbol] : request;
/**
@@ -323,7 +316,7 @@ export function isKibanaRequest(request: unknown): request is KibanaRequest {
return request instanceof KibanaRequest;
}
-function isRequest(request: any): request is LegacyRequest {
+function isRequest(request: any): request is Request {
try {
return request.raw.req && typeof request.raw.req === 'object';
} catch {
@@ -332,9 +325,9 @@ function isRequest(request: any): request is LegacyRequest {
}
/**
- * Checks if an incoming request either KibanaRequest or Legacy.Request
+ * Checks if an incoming request either KibanaRequest or Hapi.Request
* @internal
*/
-export function isRealRequest(request: unknown): request is KibanaRequest | LegacyRequest {
+export function isRealRequest(request: unknown): request is KibanaRequest | Request {
return isKibanaRequest(request) || isRequest(request);
}
diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts
index a16e0f2217add..49e97f6a40e41 100644
--- a/src/core/server/metrics/integration_tests/server_collector.test.ts
+++ b/src/core/server/metrics/integration_tests/server_collector.test.ts
@@ -19,7 +19,8 @@ import { setTimeout as setTimeoutPromise } from 'timers/promises';
const requestWaitDelay = 25;
-describe('ServerMetricsCollector', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/59234
+describe.skip('ServerMetricsCollector', () => {
let server: HttpService;
let collector: ServerMetricsCollector;
let hapiServer: HapiServer;
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/constants.ts b/src/plugins/chart_expressions/expression_heatmap/common/constants.ts
new file mode 100644
index 0000000000000..12939483e64fa
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/constants.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const EXPRESSION_HEATMAP_NAME = 'heatmap';
+export const EXPRESSION_HEATMAP_LEGEND_NAME = 'heatmap_legend';
+export const EXPRESSION_HEATMAP_GRID_NAME = 'heatmap_grid';
+export const HEATMAP_FUNCTION_RENDERER_NAME = 'heatmap_renderer';
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/__snapshots__/heatmap_function.test.ts.snap b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/__snapshots__/heatmap_function.test.ts.snap
new file mode 100644
index 0000000000000..55b7ddfcaea53
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/__snapshots__/heatmap_function.test.ts.snap
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`interpreter/functions#heatmap logs correct datatable to inspector 1`] = `
+Object {
+ "columns": Array [
+ Object {
+ "id": "col-0-1",
+ "meta": Object {
+ "dimensionName": undefined,
+ "type": "number",
+ },
+ "name": "Count",
+ },
+ Object {
+ "id": "col-1-2",
+ "meta": Object {
+ "dimensionName": undefined,
+ "type": "string",
+ },
+ "name": "Dest",
+ },
+ ],
+ "rows": Array [
+ Object {
+ "col-0-1": 0,
+ },
+ ],
+ "type": "datatable",
+}
+`;
+
+exports[`interpreter/functions#heatmap returns an object with the correct structure 1`] = `
+Object {
+ "as": "heatmap",
+ "type": "render",
+ "value": Object {
+ "args": Object {
+ "gridConfig": Object {
+ "isCellLabelVisible": true,
+ "isXAxisLabelVisible": true,
+ "isYAxisLabelVisible": true,
+ "type": "heatmap_grid",
+ },
+ "highlightInHover": false,
+ "lastRangeIsRightOpen": true,
+ "legend": Object {
+ "isVisible": true,
+ "position": "top",
+ "type": "heatmap_legend",
+ },
+ "palette": Object {
+ "name": "",
+ "params": Object {
+ "colors": Array [
+ "rgb(0, 0, 0, 0)",
+ "rgb(112, 38, 231)",
+ ],
+ "gradient": false,
+ "range": "number",
+ "rangeMax": 150,
+ "rangeMin": 0,
+ "stops": Array [
+ 0,
+ 10000,
+ ],
+ },
+ "type": "palette",
+ },
+ "percentageMode": false,
+ "showTooltip": true,
+ "splitColumnAccessor": undefined,
+ "splitRowAccessor": undefined,
+ "valueAccessor": "col-0-1",
+ "xAccessor": "col-1-2",
+ "yAccessor": undefined,
+ },
+ "data": Object {
+ "columns": Array [
+ Object {
+ "id": "col-0-1",
+ "meta": Object {
+ "type": "number",
+ },
+ "name": "Count",
+ },
+ Object {
+ "id": "col-1-2",
+ "meta": Object {
+ "type": "string",
+ },
+ "name": "Dest",
+ },
+ ],
+ "rows": Array [
+ Object {
+ "col-0-1": 0,
+ },
+ ],
+ "type": "datatable",
+ },
+ },
+}
+`;
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.test.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.test.ts
new file mode 100644
index 0000000000000..0b0cdf565dc33
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.test.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { heatmapFunction } from './heatmap_function';
+import type { HeatmapArguments } from '../../common';
+import { functionWrapper } from '../../../../expressions/common/expression_functions/specs/tests/utils';
+import { Datatable } from '../../../../expressions/common/expression_types/specs';
+import { EXPRESSION_HEATMAP_GRID_NAME, EXPRESSION_HEATMAP_LEGEND_NAME } from '../constants';
+
+describe('interpreter/functions#heatmap', () => {
+ const fn = functionWrapper(heatmapFunction());
+ const context: Datatable = {
+ type: 'datatable',
+ rows: [{ 'col-0-1': 0 }],
+ columns: [
+ { id: 'col-0-1', name: 'Count', meta: { type: 'number' } },
+ { id: 'col-1-2', name: 'Dest', meta: { type: 'string' } },
+ ],
+ };
+ const args: HeatmapArguments = {
+ percentageMode: false,
+ legend: {
+ isVisible: true,
+ position: 'top',
+ type: EXPRESSION_HEATMAP_LEGEND_NAME,
+ },
+ gridConfig: {
+ isCellLabelVisible: true,
+ isYAxisLabelVisible: true,
+ isXAxisLabelVisible: true,
+ type: EXPRESSION_HEATMAP_GRID_NAME,
+ },
+ palette: {
+ type: 'palette',
+ name: '',
+ params: {
+ colors: ['rgb(0, 0, 0, 0)', 'rgb(112, 38, 231)'],
+ stops: [0, 10000],
+ gradient: false,
+ rangeMin: 0,
+ rangeMax: 150,
+ range: 'number',
+ },
+ },
+ showTooltip: true,
+ highlightInHover: false,
+ xAccessor: 'col-1-2',
+ valueAccessor: 'col-0-1',
+ };
+
+ it('returns an object with the correct structure', () => {
+ const actual = fn(context, args, undefined);
+
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('logs correct datatable to inspector', async () => {
+ let loggedTable: Datatable;
+ const handlers = {
+ inspectorAdapters: {
+ tables: {
+ logDatatable: (name: string, datatable: Datatable) => {
+ loggedTable = datatable;
+ },
+ },
+ },
+ };
+ await fn(context, args, handlers as any);
+
+ expect(loggedTable!).toMatchSnapshot();
+ });
+});
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts
new file mode 100644
index 0000000000000..6ebec4b118c76
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts
@@ -0,0 +1,204 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import type { DatatableColumn } from '../../../../expressions/public';
+import { ExpressionValueVisDimension } from '../../../../visualizations/common';
+import { prepareLogTable, Dimension } from '../../../../visualizations/common/prepare_log_table';
+import { HeatmapExpressionFunctionDefinition } from '../types';
+import {
+ EXPRESSION_HEATMAP_NAME,
+ EXPRESSION_HEATMAP_GRID_NAME,
+ EXPRESSION_HEATMAP_LEGEND_NAME,
+} from '../constants';
+
+const convertToVisDimension = (columns: DatatableColumn[], accessor: string) => {
+ const column = columns.find((c) => c.id === accessor);
+ if (!column) return;
+ return {
+ accessor: Number(column.id),
+ format: {
+ id: column.meta.params?.id,
+ params: { ...column.meta.params?.params },
+ },
+ type: 'vis_dimension',
+ } as ExpressionValueVisDimension;
+};
+
+const prepareHeatmapLogTable = (
+ columns: DatatableColumn[],
+ accessor: string | ExpressionValueVisDimension,
+ table: Dimension[],
+ label: string
+) => {
+ const dimension =
+ typeof accessor === 'string' ? convertToVisDimension(columns, accessor) : accessor;
+ if (dimension) {
+ table.push([[dimension], label]);
+ }
+};
+
+export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({
+ name: EXPRESSION_HEATMAP_NAME,
+ type: 'render',
+ inputTypes: ['datatable'],
+ help: i18n.translate('expressionHeatmap.function.help', {
+ defaultMessage: 'Heatmap visualization',
+ }),
+ args: {
+ // used only in legacy heatmap, consider it as @deprecated
+ percentageMode: {
+ types: ['boolean'],
+ default: false,
+ help: i18n.translate('expressionHeatmap.function.percentageMode.help', {
+ defaultMessage: 'When is on, tooltip and legends appear as percentages.',
+ }),
+ },
+ palette: {
+ types: ['palette'],
+ help: i18n.translate('expressionHeatmap.function.palette.help', {
+ defaultMessage: 'Provides colors for the values, based on the bounds.',
+ }),
+ },
+ legend: {
+ types: [EXPRESSION_HEATMAP_LEGEND_NAME],
+ help: i18n.translate('expressionHeatmap.function.legendConfig.help', {
+ defaultMessage: 'Configure the chart legend.',
+ }),
+ },
+ gridConfig: {
+ types: [EXPRESSION_HEATMAP_GRID_NAME],
+ help: i18n.translate('expressionHeatmap.function.gridConfig.help', {
+ defaultMessage: 'Configure the heatmap layout.',
+ }),
+ },
+ showTooltip: {
+ types: ['boolean'],
+ help: i18n.translate('expressionHeatmap.function.args.addTooltipHelpText', {
+ defaultMessage: 'Show tooltip on hover',
+ }),
+ default: true,
+ },
+ // not supported yet
+ highlightInHover: {
+ types: ['boolean'],
+ help: i18n.translate('expressionHeatmap.function.args.highlightInHoverHelpText', {
+ defaultMessage:
+ 'When this is enabled, it highlights the ranges of the same color on legend hover',
+ }),
+ },
+ lastRangeIsRightOpen: {
+ types: ['boolean'],
+ help: i18n.translate('expressionHeatmap.function.args.lastRangeIsRightOpen', {
+ defaultMessage: 'If is set to true, the last range value will be right open',
+ }),
+ default: true,
+ },
+ xAccessor: {
+ types: ['string', 'vis_dimension'],
+ help: i18n.translate('expressionHeatmap.function.args.xAccessorHelpText', {
+ defaultMessage: 'The id of the x axis column or the corresponding dimension',
+ }),
+ },
+ yAccessor: {
+ types: ['string', 'vis_dimension'],
+
+ help: i18n.translate('expressionHeatmap.function.args.yAccessorHelpText', {
+ defaultMessage: 'The id of the y axis column or the corresponding dimension',
+ }),
+ },
+ valueAccessor: {
+ types: ['string', 'vis_dimension'],
+
+ help: i18n.translate('expressionHeatmap.function.args.valueAccessorHelpText', {
+ defaultMessage: 'The id of the value column or the corresponding dimension',
+ }),
+ },
+ // not supported yet, small multiples accessor
+ splitRowAccessor: {
+ types: ['string', 'vis_dimension'],
+
+ help: i18n.translate('expressionHeatmap.function.args.splitRowAccessorHelpText', {
+ defaultMessage: 'The id of the split row or the corresponding dimension',
+ }),
+ },
+ // not supported yet, small multiples accessor
+ splitColumnAccessor: {
+ types: ['string', 'vis_dimension'],
+
+ help: i18n.translate('expressionHeatmap.function.args.splitColumnAccessorHelpText', {
+ defaultMessage: 'The id of the split column or the corresponding dimension',
+ }),
+ },
+ },
+ fn(data, args, handlers) {
+ if (handlers?.inspectorAdapters?.tables) {
+ const argsTable: Dimension[] = [];
+ if (args.valueAccessor) {
+ prepareHeatmapLogTable(
+ data.columns,
+ args.valueAccessor,
+ argsTable,
+ i18n.translate('expressionHeatmap.function.dimension.metric', {
+ defaultMessage: 'Metric',
+ })
+ );
+ }
+ if (args.yAccessor) {
+ prepareHeatmapLogTable(
+ data.columns,
+ args.yAccessor,
+ argsTable,
+ i18n.translate('expressionHeatmap.function.dimension.yaxis', {
+ defaultMessage: 'Y axis',
+ })
+ );
+ }
+ if (args.xAccessor) {
+ prepareHeatmapLogTable(
+ data.columns,
+ args.xAccessor,
+ argsTable,
+ i18n.translate('expressionHeatmap.function.dimension.xaxis', {
+ defaultMessage: 'X axis',
+ })
+ );
+ }
+ if (args.splitRowAccessor) {
+ prepareHeatmapLogTable(
+ data.columns,
+ args.splitRowAccessor,
+ argsTable,
+ i18n.translate('expressionHeatmap.function.dimension.splitRow', {
+ defaultMessage: 'Split by row',
+ })
+ );
+ }
+ if (args.splitColumnAccessor) {
+ prepareHeatmapLogTable(
+ data.columns,
+ args.splitColumnAccessor,
+ argsTable,
+ i18n.translate('expressionHeatmap.function.dimension.splitColumn', {
+ defaultMessage: 'Split by column',
+ })
+ );
+ }
+ const logTable = prepareLogTable(data, argsTable);
+ handlers.inspectorAdapters.tables.logDatatable('default', logTable);
+ }
+ return {
+ type: 'render',
+ as: EXPRESSION_HEATMAP_NAME,
+ value: {
+ data,
+ args,
+ },
+ };
+ },
+});
diff --git a/x-pack/plugins/lens/common/expressions/heatmap_chart/heatmap_grid.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts
similarity index 54%
rename from x-pack/plugins/lens/common/expressions/heatmap_chart/heatmap_grid.ts
rename to src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts
index 5fe7f4b8f6c62..17513555d394d 100644
--- a/x-pack/plugins/lens/common/expressions/heatmap_chart/heatmap_grid.ts
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts
@@ -1,70 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
-import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common';
-
-export const HEATMAP_GRID_FUNCTION = 'lens_heatmap_grid';
-
-export interface HeatmapGridConfig {
- // grid
- strokeWidth?: number;
- strokeColor?: string;
- cellHeight?: number;
- cellWidth?: number;
- // cells
- isCellLabelVisible: boolean;
- // Y-axis
- isYAxisLabelVisible: boolean;
- yAxisLabelWidth?: number;
- yAxisLabelColor?: string;
- // X-axis
- isXAxisLabelVisible: boolean;
-}
-
-export type HeatmapGridConfigResult = HeatmapGridConfig & { type: typeof HEATMAP_GRID_FUNCTION };
+import type { ExpressionFunctionDefinition } from '../../../../expressions/common';
+import { EXPRESSION_HEATMAP_GRID_NAME } from '../constants';
+import { HeatmapGridConfig, HeatmapGridConfigResult } from '../types';
export const heatmapGridConfig: ExpressionFunctionDefinition<
- typeof HEATMAP_GRID_FUNCTION,
+ typeof EXPRESSION_HEATMAP_GRID_NAME,
null,
HeatmapGridConfig,
HeatmapGridConfigResult
> = {
- name: HEATMAP_GRID_FUNCTION,
+ name: EXPRESSION_HEATMAP_GRID_NAME,
aliases: [],
- type: HEATMAP_GRID_FUNCTION,
+ type: EXPRESSION_HEATMAP_GRID_NAME,
help: `Configure the heatmap layout `,
inputTypes: ['null'],
args: {
// grid
strokeWidth: {
types: ['number'],
- help: i18n.translate('xpack.lens.heatmapChart.config.strokeWidth.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.strokeWidth.help', {
defaultMessage: 'Specifies the grid stroke width',
}),
required: false,
},
strokeColor: {
types: ['string'],
- help: i18n.translate('xpack.lens.heatmapChart.config.strokeColor.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.strokeColor.help', {
defaultMessage: 'Specifies the grid stroke color',
}),
required: false,
},
cellHeight: {
types: ['number'],
- help: i18n.translate('xpack.lens.heatmapChart.config.cellHeight.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.cellHeight.help', {
defaultMessage: 'Specifies the grid cell height',
}),
required: false,
},
cellWidth: {
types: ['number'],
- help: i18n.translate('xpack.lens.heatmapChart.config.cellWidth.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.cellWidth.help', {
defaultMessage: 'Specifies the grid cell width',
}),
required: false,
@@ -72,27 +55,27 @@ export const heatmapGridConfig: ExpressionFunctionDefinition<
// cells
isCellLabelVisible: {
types: ['boolean'],
- help: i18n.translate('xpack.lens.heatmapChart.config.isCellLabelVisible.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.isCellLabelVisible.help', {
defaultMessage: 'Specifies whether or not the cell label is visible.',
}),
},
// Y-axis
isYAxisLabelVisible: {
types: ['boolean'],
- help: i18n.translate('xpack.lens.heatmapChart.config.isYAxisLabelVisible.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.isYAxisLabelVisible.help', {
defaultMessage: 'Specifies whether or not the Y-axis labels are visible.',
}),
},
yAxisLabelWidth: {
types: ['number'],
- help: i18n.translate('xpack.lens.heatmapChart.config.yAxisLabelWidth.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.yAxisLabelWidth.help', {
defaultMessage: 'Specifies the width of the Y-axis labels.',
}),
required: false,
},
yAxisLabelColor: {
types: ['string'],
- help: i18n.translate('xpack.lens.heatmapChart.config.yAxisLabelColor.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.yAxisLabelColor.help', {
defaultMessage: 'Specifies the color of the Y-axis labels.',
}),
required: false,
@@ -100,14 +83,14 @@ export const heatmapGridConfig: ExpressionFunctionDefinition<
// X-axis
isXAxisLabelVisible: {
types: ['boolean'],
- help: i18n.translate('xpack.lens.heatmapChart.config.isXAxisLabelVisible.help', {
+ help: i18n.translate('expressionHeatmap.function.args.grid.isXAxisLabelVisible.help', {
defaultMessage: 'Specifies whether or not the X-axis labels are visible.',
}),
},
},
fn(input, args) {
return {
- type: HEATMAP_GRID_FUNCTION,
+ type: EXPRESSION_HEATMAP_GRID_NAME,
...args,
};
},
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_legend.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_legend.ts
new file mode 100644
index 0000000000000..efbc251f6360b
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_legend.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { Position } from '@elastic/charts';
+import { i18n } from '@kbn/i18n';
+import type { ExpressionFunctionDefinition } from '../../../../expressions/common';
+import { EXPRESSION_HEATMAP_LEGEND_NAME } from '../constants';
+import { HeatmapLegendConfig, HeatmapLegendConfigResult } from '../types';
+
+export const heatmapLegendConfig: ExpressionFunctionDefinition<
+ typeof EXPRESSION_HEATMAP_LEGEND_NAME,
+ null,
+ HeatmapLegendConfig,
+ HeatmapLegendConfigResult
+> = {
+ name: EXPRESSION_HEATMAP_LEGEND_NAME,
+ aliases: [],
+ type: EXPRESSION_HEATMAP_LEGEND_NAME,
+ help: `Configure the heatmap chart's legend`,
+ inputTypes: ['null'],
+ args: {
+ isVisible: {
+ types: ['boolean'],
+ help: i18n.translate('expressionHeatmap.function.args.legend.isVisible.help', {
+ defaultMessage: 'Specifies whether or not the legend is visible.',
+ }),
+ },
+ position: {
+ types: ['string'],
+ options: [Position.Top, Position.Right, Position.Bottom, Position.Left],
+ help: i18n.translate('expressionHeatmap.function.args.legend.position.help', {
+ defaultMessage: 'Specifies the legend position.',
+ }),
+ },
+ maxLines: {
+ types: ['number'],
+ help: i18n.translate('expressionHeatmap.function.args.legend.maxLines.help', {
+ defaultMessage: 'Specifies the number of lines per legend item.',
+ }),
+ },
+ shouldTruncate: {
+ types: ['boolean'],
+ default: true,
+ help: i18n.translate('expressionHeatmap.function.args.legend.shouldTruncate.help', {
+ defaultMessage: 'Specifies whether or not the legend items should be truncated.',
+ }),
+ },
+ },
+ fn(input, args) {
+ return {
+ type: EXPRESSION_HEATMAP_LEGEND_NAME,
+ ...args,
+ };
+ },
+};
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/index.ts
new file mode 100644
index 0000000000000..f926de038a7e7
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { heatmapFunction } from './heatmap_function';
+export { heatmapLegendConfig } from './heatmap_legend';
+export { heatmapGridConfig } from './heatmap_grid';
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/index.ts b/src/plugins/chart_expressions/expression_heatmap/common/index.ts
new file mode 100755
index 0000000000000..56bafc2a0d612
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const PLUGIN_ID = 'expressionHeatmap';
+export const PLUGIN_NAME = 'expressionHeatmap';
+
+export type {
+ HeatmapExpressionProps,
+ FilterEvent,
+ BrushEvent,
+ FormatFactory,
+ HeatmapRenderProps,
+ CustomPaletteParams,
+ ColorStop,
+ RequiredPaletteParamTypes,
+ HeatmapLegendConfigResult,
+ HeatmapGridConfigResult,
+ HeatmapArguments,
+} from './types';
+
+export { heatmapFunction, heatmapLegendConfig, heatmapGridConfig } from './expression_functions';
+
+export { EXPRESSION_HEATMAP_NAME } from './constants';
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts
new file mode 100644
index 0000000000000..a983da669c56d
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { Position } from '@elastic/charts';
+import {
+ Datatable,
+ ExpressionFunctionDefinition,
+ ExpressionValueRender,
+} from '../../../../expressions';
+import { ExpressionValueVisDimension } from '../../../../visualizations/common';
+
+import { CustomPaletteState, PaletteOutput } from '../../../../charts/common';
+import {
+ EXPRESSION_HEATMAP_NAME,
+ EXPRESSION_HEATMAP_LEGEND_NAME,
+ EXPRESSION_HEATMAP_GRID_NAME,
+ HEATMAP_FUNCTION_RENDERER_NAME,
+} from '../constants';
+
+export interface HeatmapLegendConfig {
+ /**
+ * Flag whether the legend should be shown. If there is just a single series, it will be hidden
+ */
+ isVisible: boolean;
+ /**
+ * Position of the legend relative to the chart
+ */
+ position: Position;
+ /**
+ * Defines the number of lines per legend item
+ */
+ maxLines?: number;
+ /**
+ * Defines if the legend items should be truncated
+ */
+ shouldTruncate?: boolean;
+}
+
+export type HeatmapLegendConfigResult = HeatmapLegendConfig & {
+ type: typeof EXPRESSION_HEATMAP_LEGEND_NAME;
+};
+
+export interface HeatmapGridConfig {
+ // grid
+ strokeWidth?: number;
+ strokeColor?: string;
+ cellHeight?: number;
+ cellWidth?: number;
+ // cells
+ isCellLabelVisible: boolean;
+ // Y-axis
+ isYAxisLabelVisible: boolean;
+ yAxisLabelWidth?: number;
+ yAxisLabelColor?: string;
+ // X-axis
+ isXAxisLabelVisible: boolean;
+}
+
+export type HeatmapGridConfigResult = HeatmapGridConfig & {
+ type: typeof EXPRESSION_HEATMAP_GRID_NAME;
+};
+
+export interface HeatmapArguments {
+ percentageMode?: boolean;
+ lastRangeIsRightOpen?: boolean;
+ showTooltip?: boolean;
+ highlightInHover?: boolean;
+ palette?: PaletteOutput;
+ xAccessor?: string | ExpressionValueVisDimension;
+ yAccessor?: string | ExpressionValueVisDimension;
+ valueAccessor?: string | ExpressionValueVisDimension;
+ splitRowAccessor?: string | ExpressionValueVisDimension;
+ splitColumnAccessor?: string | ExpressionValueVisDimension;
+ legend: HeatmapLegendConfigResult;
+ gridConfig: HeatmapGridConfigResult;
+}
+
+export type HeatmapInput = Datatable;
+
+export interface HeatmapExpressionProps {
+ data: Datatable;
+ args: HeatmapArguments;
+}
+
+export interface HeatmapRender {
+ type: 'render';
+ as: typeof HEATMAP_FUNCTION_RENDERER_NAME;
+ value: HeatmapExpressionProps;
+}
+
+export type HeatmapExpressionFunctionDefinition = ExpressionFunctionDefinition<
+ typeof EXPRESSION_HEATMAP_NAME,
+ HeatmapInput,
+ HeatmapArguments,
+ ExpressionValueRender
+>;
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_renderers.ts
new file mode 100644
index 0000000000000..1498c04ca1b79
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_renderers.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { ChartsPluginSetup, PaletteRegistry } from '../../../../charts/public';
+import type { IFieldFormat, SerializedFieldFormat } from '../../../../field_formats/common';
+import type { RangeSelectContext, ValueClickContext } from '../../../../embeddable/public';
+import type { PersistedState } from '../../../../visualizations/public';
+import type { HeatmapExpressionProps } from './expression_functions';
+
+export interface FilterEvent {
+ name: 'filter';
+ data: ValueClickContext['data'];
+}
+
+export interface BrushEvent {
+ name: 'brush';
+ data: RangeSelectContext['data'];
+}
+
+export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
+
+export type HeatmapRenderProps = HeatmapExpressionProps & {
+ timeZone?: string;
+ formatFactory: FormatFactory;
+ chartsThemeService: ChartsPluginSetup['theme'];
+ onClickValue: (data: FilterEvent['data']) => void;
+ onSelectRange: (data: BrushEvent['data']) => void;
+ paletteService: PaletteRegistry;
+ uiState: PersistedState;
+};
+
+export interface ColorStop {
+ color: string;
+ stop: number;
+}
+
+export interface CustomPaletteParams {
+ name?: string;
+ reverse?: boolean;
+ rangeType?: 'number' | 'percent';
+ continuity?: 'above' | 'below' | 'all' | 'none';
+ progression?: 'fixed';
+ rangeMin?: number;
+ rangeMax?: number;
+ stops?: ColorStop[];
+ colorStops?: ColorStop[];
+ steps?: number;
+}
+
+export type RequiredPaletteParamTypes = Required;
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/types/index.ts b/src/plugins/chart_expressions/expression_heatmap/common/types/index.ts
new file mode 100644
index 0000000000000..9c50bfab1305d
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/common/types/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './expression_functions';
+export * from './expression_renderers';
diff --git a/src/plugins/chart_expressions/expression_heatmap/jest.config.js b/src/plugins/chart_expressions/expression_heatmap/jest.config.js
new file mode 100644
index 0000000000000..319364343ee09
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/jest.config.js
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../../',
+ roots: ['/src/plugins/chart_expressions/expression_heatmap'],
+ coverageDirectory:
+ '/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_heatmap',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/src/plugins/chart_expressions/expression_heatmap/{common,public,server}/**/*.{ts,tsx}',
+ ],
+};
diff --git a/src/plugins/chart_expressions/expression_heatmap/kibana.json b/src/plugins/chart_expressions/expression_heatmap/kibana.json
new file mode 100755
index 0000000000000..604919ec54592
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/kibana.json
@@ -0,0 +1,16 @@
+{
+ "id": "expressionHeatmap",
+ "version": "1.0.0",
+ "kibanaVersion": "kibana",
+ "owner": {
+ "name": "Vis Editors",
+ "githubTeam": "kibana-vis-editors"
+ },
+ "description": "Expression Heatmap plugin adds a `heatmap` renderer and function to the expression plugin. The renderer will display the `heatmap` chart.",
+ "server": true,
+ "ui": true,
+ "requiredPlugins": ["expressions", "fieldFormats", "charts", "visualizations", "presentationUtil", "data"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"],
+ "optionalPlugins": [],
+ "extraPublicDirs": ["common"]
+}
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx
new file mode 100644
index 0000000000000..b725f9eed3555
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx
@@ -0,0 +1,246 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { Settings, TooltipType, Heatmap } from '@elastic/charts';
+import { chartPluginMock } from '../../../../charts/public/mocks';
+import { EmptyPlaceholder } from '../../../../charts/public';
+import { fieldFormatsServiceMock } from '../../../../field_formats/public/mocks';
+import type { Datatable } from '../../../../expressions/public';
+import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { act } from 'react-dom/test-utils';
+import { HeatmapRenderProps, HeatmapArguments } from '../../common';
+import HeatmapComponent from './heatmap_component';
+
+jest.mock('@elastic/charts', () => {
+ const original = jest.requireActual('@elastic/charts');
+
+ return {
+ ...original,
+ getSpecId: jest.fn(() => {}),
+ };
+});
+
+const chartsThemeService = chartPluginMock.createSetupContract().theme;
+const palettesRegistry = chartPluginMock.createPaletteRegistry();
+const formatService = fieldFormatsServiceMock.createStartContract();
+const args: HeatmapArguments = {
+ percentageMode: false,
+ legend: {
+ isVisible: true,
+ position: 'top',
+ type: 'heatmap_legend',
+ },
+ gridConfig: {
+ isCellLabelVisible: true,
+ isYAxisLabelVisible: true,
+ isXAxisLabelVisible: true,
+ type: 'heatmap_grid',
+ },
+ palette: {
+ type: 'palette',
+ name: '',
+ params: {
+ colors: ['rgb(0, 0, 0)', 'rgb(112, 38, 231)'],
+ stops: [0, 150],
+ gradient: false,
+ rangeMin: 0,
+ rangeMax: 150,
+ range: 'number',
+ },
+ },
+ showTooltip: true,
+ highlightInHover: false,
+ xAccessor: 'col-1-2',
+ valueAccessor: 'col-0-1',
+};
+const data: Datatable = {
+ type: 'datatable',
+ rows: [
+ { 'col-0-1': 0, 'col-1-2': 'a' },
+ { 'col-0-1': 148, 'col-1-2': 'b' },
+ ],
+ columns: [
+ { id: 'col-0-1', name: 'Count', meta: { type: 'number' } },
+ { id: 'col-1-2', name: 'Dest', meta: { type: 'string' } },
+ ],
+};
+
+const mockState = new Map();
+const uiState = {
+ get: jest
+ .fn()
+ .mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)),
+ set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)),
+ emit: jest.fn(),
+ setSilent: jest.fn(),
+} as any;
+
+describe('HeatmapComponent', function () {
+ let wrapperProps: HeatmapRenderProps;
+
+ beforeAll(() => {
+ wrapperProps = {
+ data,
+ chartsThemeService,
+ args,
+ uiState,
+ onClickValue: jest.fn(),
+ onSelectRange: jest.fn(),
+ paletteService: palettesRegistry,
+ formatFactory: formatService.deserialize,
+ };
+ });
+
+ it('renders the legend on the correct position', () => {
+ const component = shallowWithIntl( );
+ expect(component.find(Settings).prop('legendPosition')).toEqual('top');
+ });
+
+ it('renders the legend toggle component if uiState is set', async () => {
+ const component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(1);
+ });
+ });
+
+ it('not renders the legend toggle component if uiState is undefined', async () => {
+ const newProps = { ...wrapperProps, uiState: undefined } as unknown as HeatmapRenderProps;
+ const component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(0);
+ });
+ });
+
+ it('renders the legendColorPicker if uiState is set', async () => {
+ const component = mountWithIntl( );
+ await act(async () => {
+ expect(component.find(Settings).prop('legendColorPicker')).toBeDefined();
+ });
+ });
+
+ it('not renders the legendColorPicker if uiState is undefined', async () => {
+ const newProps = { ...wrapperProps, uiState: undefined } as unknown as HeatmapRenderProps;
+ const component = mountWithIntl( );
+ await act(async () => {
+ expect(component.find(Settings).prop('legendColorPicker')).toBeUndefined();
+ });
+ });
+
+ it('computes the bands correctly for infinite bounds', async () => {
+ const component = mountWithIntl( );
+ await act(async () => {
+ expect(component.find(Heatmap).prop('colorScale')).toEqual({
+ bands: [
+ { color: 'rgb(0, 0, 0)', end: 0, start: 0 },
+ { color: 'rgb(112, 38, 231)', end: Infinity, start: 0 },
+ ],
+ type: 'bands',
+ });
+ });
+ });
+
+ it('computes the bands correctly for distinct bounds', async () => {
+ const newProps = {
+ ...wrapperProps,
+ args: { ...wrapperProps.args, lastRangeIsRightOpen: false },
+ } as unknown as HeatmapRenderProps;
+ const component = mountWithIntl( );
+ await act(async () => {
+ expect(component.find(Heatmap).prop('colorScale')).toEqual({
+ bands: [
+ { color: 'rgb(0, 0, 0)', end: 0, start: 0 },
+ { color: 'rgb(112, 38, 231)', end: 150, start: 0 },
+ ],
+ type: 'bands',
+ });
+ });
+ });
+
+ it('hides the legend if the legend isVisible args is false', async () => {
+ const newProps = {
+ ...wrapperProps,
+ args: { ...wrapperProps.args, legend: { ...wrapperProps.args.legend, isVisible: false } },
+ } as unknown as HeatmapRenderProps;
+ const component = mountWithIntl( );
+ expect(component.find(Settings).prop('showLegend')).toEqual(false);
+ });
+
+ it('defaults on displaying the tooltip', () => {
+ const component = shallowWithIntl( );
+ expect(component.find(Settings).prop('tooltip')).toStrictEqual({ type: TooltipType.Follow });
+ });
+
+ it('hides the legend if the showTooltip is false', async () => {
+ const newProps = {
+ ...wrapperProps,
+ args: { ...wrapperProps.args, showTooltip: false },
+ } as unknown as HeatmapRenderProps;
+ const component = mountWithIntl( );
+ expect(component.find(Settings).prop('tooltip')).toStrictEqual({ type: TooltipType.None });
+ });
+
+ it('not renders the component if no value accessor is given', () => {
+ const newProps = { ...wrapperProps, valueAccessor: undefined } as unknown as HeatmapRenderProps;
+ const component = mountWithIntl( );
+ expect(component).toEqual({});
+ });
+
+ it('renders the EmptyPlaceholder if no data are provided', () => {
+ const newData: Datatable = {
+ type: 'datatable',
+ rows: [],
+ columns: [
+ { id: 'col-0-1', name: 'Count', meta: { type: 'number' } },
+ { id: 'col-1-2', name: 'Dest', meta: { type: 'string' } },
+ ],
+ };
+ const newProps = { ...wrapperProps, data: newData };
+ const component = mountWithIntl( );
+ expect(component.find(EmptyPlaceholder).length).toBe(1);
+ });
+
+ it('calls filter callback', () => {
+ const component = shallowWithIntl( );
+ component.find(Settings).first().prop('onElementClick')!([
+ [
+ {
+ x: 436.68671874999995,
+ y: 1,
+ yIndex: 0,
+ width: 143.22890625,
+ height: 198.5,
+ datum: {
+ x: 'Vienna International Airport',
+ y: 'ES-Air',
+ value: 6,
+ originalIndex: 12,
+ },
+ fill: {
+ color: [78, 175, 98, 1],
+ },
+ stroke: {
+ color: [128, 128, 128, 1],
+ width: 0,
+ },
+ value: 6,
+ visible: true,
+ formatted: '6',
+ fontSize: 18,
+ textColor: 'rgba(0, 0, 0, 1)',
+ },
+ {
+ specId: 'heatmap',
+ key: 'spec{heatmap}',
+ },
+ ],
+ ]);
+ expect(wrapperProps.onClickValue).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx
similarity index 51%
rename from x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx
rename to src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx
index 4300208109b76..a53cb6359c800 100644
--- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx
+++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx
@@ -1,11 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
-
-import React, { FC, useMemo } from 'react';
+import React, { FC, useMemo, useState, useCallback } from 'react';
import {
Chart,
ElementClickListener,
@@ -16,23 +16,21 @@ import {
HeatmapSpec,
ScaleType,
Settings,
+ TooltipType,
+ TooltipProps,
ESFixedIntervalUnit,
ESCalendarIntervalUnit,
} from '@elastic/charts';
-import type { CustomPaletteState } from 'src/plugins/charts/public';
-import { VisualizationContainer } from '../visualization_container';
-import type { HeatmapRenderProps } from './types';
-import './index.scss';
-import type { LensBrushEvent, LensFilterEvent } from '../types';
-import {
- applyPaletteParams,
- defaultPaletteParams,
- EmptyPlaceholder,
- findMinMaxByColumnId,
-} from '../shared_components';
-import { LensIconChartHeatmap } from '../assets/chart_heatmap';
-import { DEFAULT_PALETTE_NAME } from './constants';
-import { search } from '../../../../../src/plugins/data/public';
+import type { CustomPaletteState } from '../../../../charts/public';
+import { search } from '../../../../data/public';
+import { LegendToggle, EmptyPlaceholder } from '../../../../charts/public';
+import type { DatatableColumn } from '../../../../expressions/public';
+import { ExpressionValueVisDimension } from '../../../../visualizations/public';
+import type { HeatmapRenderProps, FilterEvent, BrushEvent } from '../../common';
+import { applyPaletteParams, findMinMaxByColumnId, getSortPredicate } from './helpers';
+import { getColorPicker } from '../utils/get_color_picker';
+import { DEFAULT_PALETTE_NAME, defaultPaletteParams } from '../constants';
+import { HeatmapIcon } from './heatmap_icon';
declare global {
interface Window {
@@ -113,7 +111,18 @@ function computeColorRanges(
return { colors, ranges };
}
-export const HeatmapComponent: FC = ({
+const getAccessor = (value: string | ExpressionValueVisDimension, columns: DatatableColumn[]) => {
+ if (typeof value === 'string') {
+ return value;
+ }
+ const accessor = value.accessor;
+ if (typeof accessor === 'number') {
+ return columns[accessor].id;
+ }
+ return accessor.id;
+};
+
+const HeatmapComponent: FC = ({
data,
args,
timeZone,
@@ -122,33 +131,73 @@ export const HeatmapComponent: FC = ({
onClickValue,
onSelectRange,
paletteService,
+ uiState,
}) => {
const chartTheme = chartsThemeService.useChartsTheme();
const isDarkTheme = chartsThemeService.useDarkMode();
+ // legacy heatmap legend is handled by the uiState
+ const [showLegend, setShowLegend] = useState(() => {
+ const bwcLegendStateDefault = args.legend.isVisible ?? true;
+ return uiState?.get('vis.legendOpen', bwcLegendStateDefault);
+ });
+
+ const toggleLegend = useCallback(() => {
+ setShowLegend((value) => {
+ const newValue = !value;
+ uiState?.set?.('vis.legendOpen', newValue);
+ return newValue;
+ });
+ }, [uiState]);
+
+ const setColor = useCallback(
+ (newColor: string | null, seriesLabel: string | number) => {
+ const colors = uiState?.get('vis.colors') || {};
+ if (colors[seriesLabel] === newColor || !newColor) {
+ delete colors[seriesLabel];
+ } else {
+ colors[seriesLabel] = newColor;
+ }
+ uiState?.setSilent('vis.colors', null);
+ uiState?.set('vis.colors', colors);
+ uiState?.emit('reload');
+ uiState?.emit('colorChanged');
+ },
+ [uiState]
+ );
- const tableId = Object.keys(data.tables)[0];
- const table = data.tables[tableId];
+ const legendColorPicker = useMemo(
+ () => getColorPicker(args.legend.position, setColor, uiState),
+ [args.legend.position, setColor, uiState]
+ );
+ const table = data;
+ const valueAccessor = args.valueAccessor
+ ? getAccessor(args.valueAccessor, table.columns)
+ : undefined;
+ const minMaxByColumnId = useMemo(
+ () => findMinMaxByColumnId([valueAccessor!], table),
+ [valueAccessor, table]
+ );
const paletteParams = args.palette?.params;
+ const xAccessor = args.xAccessor ? getAccessor(args.xAccessor, table.columns) : undefined;
+ const yAccessor = args.yAccessor ? getAccessor(args.yAccessor, table.columns) : undefined;
- const xAxisColumnIndex = table.columns.findIndex((v) => v.id === args.xAccessor);
- const yAxisColumnIndex = table.columns.findIndex((v) => v.id === args.yAccessor);
+ const xAxisColumnIndex = table.columns.findIndex((v) => v.id === xAccessor);
+ const yAxisColumnIndex = table.columns.findIndex((v) => v.id === yAccessor);
const xAxisColumn = table.columns[xAxisColumnIndex];
const yAxisColumn = table.columns[yAxisColumnIndex];
- const valueColumn = table.columns.find((v) => v.id === args.valueAccessor);
+ const valueColumn = table.columns.find((v) => v.id === valueAccessor);
- const minMaxByColumnId = useMemo(
- () => findMinMaxByColumnId([args.valueAccessor!], table),
- [args.valueAccessor, table]
- );
-
- if (!xAxisColumn || !valueColumn) {
+ if (!valueColumn) {
// Chart is not ready
return null;
}
- let chartData = table.rows.filter((v) => typeof v[args.valueAccessor!] === 'number');
+ let chartData = table.rows.filter((v) => typeof v[valueAccessor!] === 'number');
+ if (!chartData || !chartData.length) {
+ return ;
+ }
if (!yAxisColumn) {
// required for tooltip
@@ -159,17 +208,23 @@ export const HeatmapComponent: FC = ({
};
});
}
-
- const xAxisMeta = xAxisColumn.meta;
- const isTimeBasedSwimLane = xAxisMeta.type === 'date';
+ const { min, max } = minMaxByColumnId[valueAccessor!];
+ // formatters
+ const xAxisMeta = xAxisColumn?.meta;
+ const xValuesFormatter = formatFactory(xAxisMeta?.params);
+ const metricFormatter = formatFactory(
+ typeof args.valueAccessor === 'string' ? valueColumn.meta.params : args?.valueAccessor?.format
+ );
+ const isTimeBasedSwimLane = xAxisMeta?.type === 'date';
+ const dateHistogramMeta = xAxisColumn
+ ? search.aggs.getDateHistogramMetaDataByDatatableColumn(xAxisColumn)
+ : undefined;
// Fallback to the ordinal scale type when a single row of data is provided.
// Related issue https://github.com/elastic/elastic-charts/issues/1184
-
let xScale: HeatmapSpec['xScale'] = { type: ScaleType.Ordinal };
if (isTimeBasedSwimLane && chartData.length > 1) {
- const dateInterval =
- search.aggs.getDateHistogramMetaDataByDatatableColumn(xAxisColumn)?.interval;
+ const dateInterval = dateHistogramMeta?.interval;
const esInterval = dateInterval ? search.aggs.parseEsInterval(dateInterval) : undefined;
if (esInterval) {
xScale = {
@@ -190,24 +245,48 @@ export const HeatmapComponent: FC = ({
}
}
- const xValuesFormatter = formatFactory(xAxisMeta.params);
- const valueFormatter = formatFactory(valueColumn.meta.params);
+ const tooltip: TooltipProps = {
+ type: args.showTooltip ? TooltipType.Follow : TooltipType.None,
+ };
+
+ const valueFormatter = (d: number) => {
+ let value = d;
+
+ if (args.percentageMode) {
+ const percentageNumber = (Math.abs(value - min) / (max - min)) * 100;
+ value = parseInt(percentageNumber.toString(), 10) / 100;
+ }
+ return metricFormatter.convert(value);
+ };
const { colors, ranges } = computeColorRanges(
paletteService,
paletteParams,
isDarkTheme ? '#000' : '#fff',
- minMaxByColumnId[args.valueAccessor!]
+ minMaxByColumnId[valueAccessor!]
);
+ // adds a very small number to the max value to make sure the max value will be included
+ const endValue =
+ paletteParams && paletteParams.range === 'number' ? paletteParams.rangeMax : max + 0.00000001;
+ const overwriteColors = uiState?.get('vis.colors') ?? null;
+
const bands = ranges.map((start, index, array) => {
+ // by default the last range is right-open
+ let end = index === array.length - 1 ? Infinity : array[index + 1];
+ // if the lastRangeIsRightOpen is set to false, we need to set the last range to the max value
+ if (args.lastRangeIsRightOpen === false) {
+ const lastBand = max === start ? Infinity : endValue;
+ end = index === array.length - 1 ? lastBand : array[index + 1];
+ }
+ const overwriteArrayIdx = `${metricFormatter.convert(start)} - ${metricFormatter.convert(end)}`;
+ const overwriteColor = overwriteColors?.[overwriteArrayIdx];
return {
// with the default continuity:above the every range is left-closed
start,
- // with the default continuity:above the last range is right-open
- end: index === array.length - 1 ? Infinity : array[index + 1],
+ end,
// the current colors array contains a duplicated color at the beginning that we need to skip
- color: colors[index + 1],
+ color: overwriteColor ?? colors[index + 1],
};
});
@@ -215,7 +294,7 @@ export const HeatmapComponent: FC = ({
const cell = e[0][0];
const { x, y } = cell.datum;
- const xAxisFieldName = xAxisColumn.meta.field;
+ const xAxisFieldName = xAxisColumn?.meta?.field;
const timeFieldName = isTimeBasedSwimLane ? xAxisFieldName : '';
const points = [
@@ -235,7 +314,7 @@ export const HeatmapComponent: FC = ({
: []),
];
- const context: LensFilterEvent['data'] = {
+ const context: FilterEvent['data'] = {
data: points.map((point) => ({
row: point.row,
column: point.column,
@@ -250,11 +329,11 @@ export const HeatmapComponent: FC = ({
const onBrushEnd = (e: HeatmapBrushEvent) => {
const { x, y } = e;
- const xAxisFieldName = xAxisColumn.meta.field;
+ const xAxisFieldName = xAxisColumn?.meta?.field;
const timeFieldName = isTimeBasedSwimLane ? xAxisFieldName : '';
if (isTimeBasedSwimLane) {
- const context: LensBrushEvent['data'] = {
+ const context: BrushEvent['data'] = {
range: x as number[],
table,
column: xAxisColumnIndex,
@@ -273,16 +352,17 @@ export const HeatmapComponent: FC = ({
});
});
}
-
- (x as string[]).forEach((v) => {
- points.push({
- row: table.rows.findIndex((r) => r[xAxisColumn.id] === v),
- column: xAxisColumnIndex,
- value: v,
+ if (xAxisColumn) {
+ (x as string[]).forEach((v) => {
+ points.push({
+ row: table.rows.findIndex((r) => r[xAxisColumn.id] === v),
+ column: xAxisColumnIndex,
+ value: v,
+ });
});
- });
+ }
- const context: LensFilterEvent['data'] = {
+ const context: FilterEvent['data'] = {
data: points.map((point) => ({
row: point.row,
column: point.column,
@@ -334,11 +414,12 @@ export const HeatmapComponent: FC = ({
: {}),
},
xAxisLabel: {
- visible: args.gridConfig.isXAxisLabelVisible,
+ visible: Boolean(args.gridConfig.isXAxisLabelVisible && xAxisColumn),
// eui color subdued
textColor: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`,
+ padding: xAxisColumn?.name ? 8 : 0,
formatter: (v: number | string) => xValuesFormatter.convert(v),
- name: xAxisColumn.name,
+ name: xAxisColumn?.name ?? '',
},
brushMask: {
fill: isDarkTheme ? 'rgb(30,31,35,80%)' : 'rgb(247,247,247,50%)',
@@ -349,56 +430,65 @@ export const HeatmapComponent: FC = ({
timeZone,
};
- if (!chartData || !chartData.length) {
- return ;
- }
-
return (
-
-
- valueFormatter.convert(v)}
- xScale={xScale}
- ySortPredicate="dataIndex"
- config={config}
- xSortPredicate="dataIndex"
- />
-
+ <>
+ {showLegend !== undefined && (
+
+ )}
+
+
+
+
+ >
);
};
-const MemoizedChart = React.memo(HeatmapComponent);
-
-export function HeatmapChartReportable(props: HeatmapRenderProps) {
- return (
-
-
-
- );
-}
+// default export required for React.Lazy
+// eslint-disable-next-line import/no-default-export
+export { HeatmapComponent as default };
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_icon.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_icon.tsx
new file mode 100644
index 0000000000000..e6ff3a65e02bb
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_icon.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiIconProps } from '@elastic/eui';
+import React from 'react';
+
+export const HeatmapIcon = ({ title, titleId, ...props }: Omit) => (
+
+ {title ? {title} : null}
+
+
+
+);
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/helpers.test.ts b/src/plugins/chart_expressions/expression_heatmap/public/components/helpers.test.ts
new file mode 100644
index 0000000000000..7e9ccee19aa11
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/components/helpers.test.ts
@@ -0,0 +1,433 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { chartPluginMock } from 'src/plugins/charts/public/mocks';
+import type { DatatableColumn } from 'src/plugins/expressions/public';
+import {
+ applyPaletteParams,
+ getDataMinMax,
+ getPaletteStops,
+ getStepValue,
+ remapStopsByNewInterval,
+ reversePalette,
+ getSortPredicate,
+} from './helpers';
+
+describe('applyPaletteParams', () => {
+ const paletteRegistry = chartPluginMock.createPaletteRegistry();
+ it('should return a palette stops array only by the name', () => {
+ expect(
+ applyPaletteParams(
+ paletteRegistry,
+ { name: 'default', type: 'palette', params: { name: 'default' } },
+ { min: 0, max: 100 }
+ )
+ ).toEqual([
+ // stops are 0 and 50 by with a 20 offset (100 divided by 5 steps) for display
+ // the mock palette service has only 2 colors so tests are a bit off by that
+ { color: 'red', stop: 20 },
+ { color: 'black', stop: 70 },
+ ]);
+ });
+
+ it('should return a palette stops array reversed', () => {
+ expect(
+ applyPaletteParams(
+ paletteRegistry,
+ { name: 'default', type: 'palette', params: { name: 'default', reverse: true } },
+ { min: 0, max: 100 }
+ )
+ ).toEqual([
+ { color: 'black', stop: 20 },
+ { color: 'red', stop: 70 },
+ ]);
+ });
+
+ it('should pick the default palette from the activePalette object when passed', () => {
+ expect(
+ applyPaletteParams(paletteRegistry, { name: 'mocked', type: 'palette' }, { min: 0, max: 100 })
+ ).toEqual([
+ { color: 'blue', stop: 20 },
+ { color: 'yellow', stop: 70 },
+ ]);
+ });
+});
+
+describe('remapStopsByNewInterval', () => {
+ it('should correctly remap the current palette from 0..1 to 0...100', () => {
+ expect(
+ remapStopsByNewInterval(
+ [
+ { color: 'black', stop: 0 },
+ { color: 'green', stop: 0.5 },
+ { color: 'red', stop: 0.9 },
+ ],
+ { newInterval: 100, oldInterval: 1, newMin: 0, oldMin: 0 }
+ )
+ ).toEqual([
+ { color: 'black', stop: 0 },
+ { color: 'green', stop: 50 },
+ { color: 'red', stop: 90 },
+ ]);
+
+ // now test the other way around
+ expect(
+ remapStopsByNewInterval(
+ [
+ { color: 'black', stop: 0 },
+ { color: 'green', stop: 50 },
+ { color: 'red', stop: 90 },
+ ],
+ { newInterval: 1, oldInterval: 100, newMin: 0, oldMin: 0 }
+ )
+ ).toEqual([
+ { color: 'black', stop: 0 },
+ { color: 'green', stop: 0.5 },
+ { color: 'red', stop: 0.9 },
+ ]);
+ });
+
+ it('should correctly handle negative numbers to/from', () => {
+ expect(
+ remapStopsByNewInterval(
+ [
+ { color: 'black', stop: -100 },
+ { color: 'green', stop: -50 },
+ { color: 'red', stop: -1 },
+ ],
+ { newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -100 }
+ )
+ ).toEqual([
+ { color: 'black', stop: 0 },
+ { color: 'green', stop: 50 },
+ { color: 'red', stop: 99 },
+ ]);
+
+ // now map the other way around
+ expect(
+ remapStopsByNewInterval(
+ [
+ { color: 'black', stop: 0 },
+ { color: 'green', stop: 50 },
+ { color: 'red', stop: 99 },
+ ],
+ { newInterval: 100, oldInterval: 100, newMin: -100, oldMin: 0 }
+ )
+ ).toEqual([
+ { color: 'black', stop: -100 },
+ { color: 'green', stop: -50 },
+ { color: 'red', stop: -1 },
+ ]);
+
+ // and test also palettes that also contains negative values
+ expect(
+ remapStopsByNewInterval(
+ [
+ { color: 'black', stop: -50 },
+ { color: 'green', stop: 0 },
+ { color: 'red', stop: 50 },
+ ],
+ { newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -50 }
+ )
+ ).toEqual([
+ { color: 'black', stop: 0 },
+ { color: 'green', stop: 50 },
+ { color: 'red', stop: 100 },
+ ]);
+ });
+});
+
+describe('getDataMinMax', () => {
+ it('should pick the correct min/max based on the current range type', () => {
+ expect(getDataMinMax('percent', { min: -100, max: 0 })).toEqual({ min: 0, max: 100 });
+ });
+
+ it('should pick the correct min/max apply percent by default', () => {
+ expect(getDataMinMax(undefined, { min: -100, max: 0 })).toEqual({ min: 0, max: 100 });
+ });
+});
+
+describe('getPaletteStops', () => {
+ const paletteRegistry = chartPluginMock.createPaletteRegistry();
+ it('should correctly compute a predefined palette stops definition from only the name', () => {
+ expect(
+ getPaletteStops(paletteRegistry, { name: 'mock' }, { dataBounds: { min: 0, max: 100 } })
+ ).toEqual([
+ { color: 'blue', stop: 20 },
+ { color: 'yellow', stop: 70 },
+ ]);
+ });
+
+ it('should correctly compute a predefined palette stops definition from explicit prevPalette (override)', () => {
+ expect(
+ getPaletteStops(
+ paletteRegistry,
+ { name: 'default' },
+ { dataBounds: { min: 0, max: 100 }, prevPalette: 'mock' }
+ )
+ ).toEqual([
+ { color: 'blue', stop: 20 },
+ { color: 'yellow', stop: 70 },
+ ]);
+ });
+
+ it('should infer the domain from dataBounds but start from 0', () => {
+ expect(
+ getPaletteStops(
+ paletteRegistry,
+ { name: 'default', rangeType: 'number' },
+ { dataBounds: { min: 1, max: 11 }, prevPalette: 'mock' }
+ )
+ ).toEqual([
+ { color: 'blue', stop: 2 },
+ { color: 'yellow', stop: 7 },
+ ]);
+ });
+
+ it('should override the minStop when requested', () => {
+ expect(
+ getPaletteStops(
+ paletteRegistry,
+ { name: 'default', rangeType: 'number' },
+ { dataBounds: { min: 1, max: 11 }, mapFromMinValue: true }
+ )
+ ).toEqual([
+ { color: 'red', stop: 1 },
+ { color: 'black', stop: 6 },
+ ]);
+ });
+
+ it('should compute a display stop palette from custom colorStops defined by the user', () => {
+ expect(
+ getPaletteStops(
+ paletteRegistry,
+ {
+ name: 'custom',
+ rangeType: 'number',
+ colorStops: [
+ { color: 'green', stop: 0 },
+ { color: 'blue', stop: 40 },
+ { color: 'red', stop: 80 },
+ ],
+ },
+ { dataBounds: { min: 0, max: 100 } }
+ )
+ ).toEqual([
+ { color: 'green', stop: 40 },
+ { color: 'blue', stop: 80 },
+ { color: 'red', stop: 100 },
+ ]);
+ });
+
+ it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end', () => {
+ expect(
+ getPaletteStops(
+ paletteRegistry,
+ {
+ name: 'custom',
+ rangeType: 'number',
+ colorStops: [
+ { color: 'green', stop: 0 },
+ { color: 'blue', stop: 40 },
+ { color: 'red', stop: 100 },
+ ],
+ },
+ { dataBounds: { min: 0, max: 100 } }
+ )
+ ).toEqual([
+ { color: 'green', stop: 40 },
+ { color: 'blue', stop: 100 },
+ { color: 'red', stop: 101 },
+ ]);
+ });
+
+ it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end (fractional)', () => {
+ expect(
+ getPaletteStops(
+ paletteRegistry,
+ {
+ name: 'custom',
+ rangeType: 'number',
+ colorStops: [
+ { color: 'green', stop: 0 },
+ { color: 'blue', stop: 0.4 },
+ { color: 'red', stop: 1 },
+ ],
+ },
+ { dataBounds: { min: 0, max: 1 } }
+ )
+ ).toEqual([
+ { color: 'green', stop: 0.4 },
+ { color: 'blue', stop: 1 },
+ { color: 'red', stop: 2 },
+ ]);
+ });
+
+ it('should compute a display stop palette from custom colorStops defined by the user - stretch the stops to 100% percent', () => {
+ expect(
+ getPaletteStops(
+ paletteRegistry,
+ {
+ name: 'custom',
+ colorStops: [
+ { color: 'green', stop: 0 },
+ { color: 'blue', stop: 0.4 },
+ { color: 'red', stop: 1 },
+ ],
+ },
+ { dataBounds: { min: 0, max: 1 } }
+ )
+ ).toEqual([
+ { color: 'green', stop: 0.4 },
+ { color: 'blue', stop: 1 },
+ { color: 'red', stop: 100 }, // default rangeType is percent, hence stretch to 100%
+ ]);
+ });
+});
+
+describe('reversePalette', () => {
+ it('should correctly reverse color and stops', () => {
+ expect(
+ reversePalette([
+ { color: 'red', stop: 0 },
+ { color: 'green', stop: 0.5 },
+ { color: 'blue', stop: 0.9 },
+ ])
+ ).toEqual([
+ { color: 'blue', stop: 0 },
+ { color: 'green', stop: 0.5 },
+ { color: 'red', stop: 0.9 },
+ ]);
+ });
+});
+
+describe('getStepValue', () => {
+ it('should compute the next step based on the last 2 stops', () => {
+ expect(
+ getStepValue(
+ // first arg is taken as max reference
+ [
+ { color: 'red', stop: 0 },
+ { color: 'red', stop: 50 },
+ ],
+ [
+ { color: 'red', stop: 0 },
+ { color: 'red', stop: 50 },
+ ],
+ 100
+ )
+ ).toBe(50);
+
+ expect(
+ getStepValue(
+ // first arg is taken as max reference
+ [
+ { color: 'red', stop: 0 },
+ { color: 'red', stop: 80 },
+ ],
+ [
+ { color: 'red', stop: 0 },
+ { color: 'red', stop: 50 },
+ ],
+ 90
+ )
+ ).toBe(10); // 90 - 80
+
+ expect(
+ getStepValue(
+ // first arg is taken as max reference
+ [
+ { color: 'red', stop: 0 },
+ { color: 'red', stop: 100 },
+ ],
+ [
+ { color: 'red', stop: 0 },
+ { color: 'red', stop: 50 },
+ ],
+ 100
+ )
+ ).toBe(1);
+ });
+});
+
+describe('getSortPredicate', () => {
+ it('should return dataIndex if otherbucker it enabled', () => {
+ const column = {
+ id: '0c4cfb78-3c2f-4eaf-82b3-4b2c1c6abe5a',
+ name: 'Top values of Carrier',
+ meta: {
+ type: 'string',
+ source: 'esaggs',
+ sourceParams: {
+ params: {
+ field: 'Carrier',
+ orderBy: '1',
+ order: 'desc',
+ size: 3,
+ otherBucket: true,
+ otherBucketLabel: 'Other',
+ missingBucket: false,
+ missingBucketLabel: '(missing value)',
+ },
+ schema: 'segment',
+ },
+ },
+ } as DatatableColumn;
+ expect(getSortPredicate(column)).toEqual('dataIndex');
+ });
+
+ it('should return numDesc for descending metric sorting', () => {
+ const column = {
+ id: 'col-0-2',
+ name: 'Dest: Descending',
+ meta: {
+ type: 'string',
+ source: 'esaggs',
+ sourceParams: {
+ params: {
+ field: 'Dest',
+ orderBy: '1',
+ order: 'desc',
+ size: 5,
+ otherBucket: false,
+ otherBucketLabel: 'Other',
+ missingBucket: false,
+ missingBucketLabel: 'Missing',
+ },
+ schema: 'segment',
+ },
+ },
+ } as DatatableColumn;
+ expect(getSortPredicate(column)).toEqual('numDesc');
+ });
+
+ it('should return alphaAsc for ascending alphabetical sorting', () => {
+ const column = {
+ id: 'col-0-2',
+ name: 'Dest: Ascending',
+ meta: {
+ type: 'string',
+ source: 'esaggs',
+ sourceParams: {
+ params: {
+ field: 'Dest',
+ orderBy: '_key',
+ order: 'asc',
+ size: 5,
+ otherBucket: false,
+ otherBucketLabel: 'Other',
+ missingBucket: false,
+ missingBucketLabel: 'Missing',
+ },
+ schema: 'segment',
+ },
+ },
+ } as DatatableColumn;
+ expect(getSortPredicate(column)).toEqual('alphaAsc');
+ });
+});
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/helpers.ts b/src/plugins/chart_expressions/expression_heatmap/public/components/helpers.ts
new file mode 100644
index 0000000000000..c9bfa68da9f22
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/components/helpers.ts
@@ -0,0 +1,248 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
+import type { Datatable, DatatableColumn } from 'src/plugins/expressions/public';
+import type { CustomPaletteParams, ColorStop } from '../../common';
+import {
+ CUSTOM_PALETTE,
+ defaultPaletteParams,
+ DEFAULT_MAX_STOP,
+ DEFAULT_MIN_STOP,
+} from '../constants';
+
+// very simple heuristic: pick last two stops and compute a new stop based on the same distance
+// if the new stop is above max, then reduce the step to reach max, or if zero then just 1.
+//
+// it accepts two series of stops as the function is used also when computing stops from colorStops
+export function getStepValue(colorStops: ColorStop[], newColorStops: ColorStop[], max: number) {
+ const length = newColorStops.length;
+ // workout the steps from the last 2 items
+ const dataStep = newColorStops[length - 1].stop - newColorStops[length - 2].stop || 1;
+ let step = Number(dataStep.toFixed(2));
+ if (max < colorStops[length - 1].stop + step) {
+ const diffToMax = max - colorStops[length - 1].stop;
+ // if the computed step goes way out of bound, fallback to 1, otherwise reach max
+ step = diffToMax > 0 ? diffToMax : 1;
+ }
+ return step;
+}
+
+// Need to shift the Custom palette in order to correctly visualize it when in display mode
+function shiftPalette(stops: ColorStop[], max: number) {
+ // shift everything right and add an additional stop at the end
+ const result = stops.map((entry, i, array) => ({
+ ...entry,
+ stop: i + 1 < array.length ? array[i + 1].stop : max,
+ }));
+ if (stops[stops.length - 1].stop === max) {
+ // extends the range by a fair amount to make it work the extra case for the last stop === max
+ const computedStep = getStepValue(stops, result, max) || 1;
+ // do not go beyond the unit step in this case
+ const step = Math.min(1, computedStep);
+ result[stops.length - 1].stop = max + step;
+ }
+ return result;
+}
+
+function getOverallMinMax(
+ params: CustomPaletteParams | undefined,
+ dataBounds: { min: number; max: number }
+) {
+ const { min: dataMin, max: dataMax } = getDataMinMax(params?.rangeType, dataBounds);
+ const minStopValue = params?.colorStops?.[0]?.stop ?? Infinity;
+ const maxStopValue = params?.colorStops?.[params.colorStops.length - 1]?.stop ?? -Infinity;
+ const overallMin = Math.min(dataMin, minStopValue);
+ const overallMax = Math.max(dataMax, maxStopValue);
+ return { min: overallMin, max: overallMax };
+}
+
+export function getDataMinMax(
+ rangeType: CustomPaletteParams['rangeType'] | undefined,
+ dataBounds: { min: number; max: number }
+) {
+ const dataMin = rangeType === 'number' ? dataBounds.min : DEFAULT_MIN_STOP;
+ const dataMax = rangeType === 'number' ? dataBounds.max : DEFAULT_MAX_STOP;
+ return { min: dataMin, max: dataMax };
+}
+// Utility to remap color stops within new domain
+export function remapStopsByNewInterval(
+ controlStops: ColorStop[],
+ {
+ newInterval,
+ oldInterval,
+ newMin,
+ oldMin,
+ }: { newInterval: number; oldInterval: number; newMin: number; oldMin: number }
+) {
+ return (controlStops || []).map(({ color, stop }) => {
+ return {
+ color,
+ stop: newMin + ((stop - oldMin) * newInterval) / oldInterval,
+ };
+ });
+}
+/**
+ * This is a generic function to compute stops from the current parameters.
+ */
+export function getPaletteStops(
+ palettes: PaletteRegistry,
+ activePaletteParams: CustomPaletteParams,
+ // used to customize color resolution
+ {
+ prevPalette,
+ dataBounds,
+ mapFromMinValue,
+ defaultPaletteName,
+ }: {
+ prevPalette?: string;
+ dataBounds: { min: number; max: number };
+ mapFromMinValue?: boolean;
+ defaultPaletteName?: string;
+ }
+) {
+ const { min: minValue, max: maxValue } = getOverallMinMax(activePaletteParams, dataBounds);
+ const interval = maxValue - minValue;
+ const { stops: currentStops, ...otherParams } = activePaletteParams || {};
+
+ if (activePaletteParams.name === 'custom' && activePaletteParams?.colorStops) {
+ // need to generate the palette from the existing controlStops
+ return shiftPalette(activePaletteParams.colorStops, maxValue);
+ }
+ // generate a palette from predefined ones and customize the domain
+ const colorStopsFromPredefined = palettes
+ .get(
+ prevPalette || activePaletteParams?.name || defaultPaletteName || defaultPaletteParams.name
+ )
+ .getCategoricalColors(defaultPaletteParams.steps, otherParams);
+
+ const newStopsMin = mapFromMinValue ? minValue : interval / defaultPaletteParams.steps;
+
+ const stops = remapStopsByNewInterval(
+ colorStopsFromPredefined.map((color, index) => ({ color, stop: index })),
+ {
+ newInterval: interval,
+ oldInterval: colorStopsFromPredefined.length,
+ newMin: newStopsMin,
+ oldMin: 0,
+ }
+ );
+ return stops;
+}
+
+export function reversePalette(paletteColorRepresentation: ColorStop[] = []) {
+ const stops = paletteColorRepresentation.map(({ stop }) => stop);
+ return paletteColorRepresentation
+ .map(({ color }, i) => ({
+ color,
+ stop: stops[paletteColorRepresentation.length - i - 1],
+ }))
+ .reverse();
+}
+
+/**
+ * Some name conventions here:
+ * * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component.
+ * * `stops` => final steps used to table coloring. It is a rightShift of the colorStops
+ * * `colorStops` => user's color stop inputs. Used to compute range min.
+ *
+ * When the user inputs the colorStops, they are designed to be the initial part of the color segment,
+ * so the next stops indicate where the previous stop ends.
+ * Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`,
+ * by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`.
+ * EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with
+ * some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok.
+ *
+ * These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening
+ * for a single change.
+ */
+
+export function applyPaletteParams>(
+ palettes: PaletteRegistry,
+ activePalette: T,
+ dataBounds: { min: number; max: number }
+) {
+ // make a copy of it as they have to be manipulated later on
+ let displayStops = getPaletteStops(palettes, activePalette?.params || {}, {
+ dataBounds,
+ defaultPaletteName: activePalette?.name,
+ });
+
+ if (activePalette?.params?.reverse && activePalette?.params?.name !== CUSTOM_PALETTE) {
+ displayStops = reversePalette(displayStops);
+ }
+ return displayStops;
+}
+
+function getId(id: string) {
+ return id;
+}
+
+export function getNumericValue(rowValue: number | number[] | undefined) {
+ if (rowValue == null || Array.isArray(rowValue)) {
+ return;
+ }
+ return rowValue;
+}
+
+export const findMinMaxByColumnId = (
+ columnIds: string[],
+ table: Datatable | undefined,
+ getOriginalId: (id: string) => string = getId
+) => {
+ const minMax: Record = {};
+
+ if (table != null) {
+ for (const columnId of columnIds) {
+ const originalId = getOriginalId(columnId);
+ minMax[originalId] = minMax[originalId] || { max: -Infinity, min: Infinity };
+ table.rows.forEach((row) => {
+ const rowValue = row[columnId];
+ const numericValue = getNumericValue(rowValue);
+ if (numericValue != null) {
+ if (minMax[originalId].min > numericValue) {
+ minMax[originalId].min = numericValue;
+ }
+ if (minMax[originalId].max < numericValue) {
+ minMax[originalId].max = numericValue;
+ }
+ }
+ });
+ // what happens when there's no data in the table? Fallback to a percent range
+ if (minMax[originalId].max === -Infinity) {
+ minMax[originalId] = { max: 100, min: 0, fallback: true };
+ }
+ }
+ }
+ return minMax;
+};
+interface SourceParams {
+ order?: string;
+ orderBy?: string;
+ otherBucket?: boolean;
+}
+
+export const getSortPredicate = (column: DatatableColumn) => {
+ const params = column.meta?.sourceParams?.params as SourceParams | undefined;
+ const sort: string | undefined = params?.orderBy;
+ if (params?.otherBucket || !sort) return 'dataIndex';
+ // metric sorting
+ if (sort && sort !== '_key') {
+ if (params?.order === 'desc') {
+ return 'numDesc';
+ } else {
+ return 'numAsc';
+ }
+ // alphabetical sorting
+ } else {
+ if (params?.order === 'desc') {
+ return 'alphaDesc';
+ } else {
+ return 'alphaAsc';
+ }
+ }
+};
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/constants.ts b/src/plugins/chart_expressions/expression_heatmap/public/constants.ts
new file mode 100644
index 0000000000000..79e24d05e3fde
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/constants.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import type { RequiredPaletteParamTypes } from '../common';
+export const DEFAULT_PALETTE_NAME = 'temperature';
+export const FIXED_PROGRESSION = 'fixed' as const;
+export const CUSTOM_PALETTE = 'custom';
+export const DEFAULT_CONTINUITY = 'above';
+export const DEFAULT_MIN_STOP = 0;
+export const DEFAULT_MAX_STOP = 100;
+export const DEFAULT_COLOR_STEPS = 5;
+export const defaultPaletteParams: RequiredPaletteParamTypes = {
+ name: DEFAULT_PALETTE_NAME,
+ reverse: false,
+ rangeType: 'percent',
+ rangeMin: DEFAULT_MIN_STOP,
+ rangeMax: DEFAULT_MAX_STOP,
+ progression: FIXED_PROGRESSION,
+ stops: [],
+ steps: DEFAULT_COLOR_STEPS,
+ colorStops: [],
+ continuity: DEFAULT_CONTINUITY,
+};
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx
new file mode 100644
index 0000000000000..efdb6aee7782d
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { i18n } from '@kbn/i18n';
+import React, { memo } from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import type { PersistedState } from '../../../../visualizations/public';
+import { ThemeServiceStart } from '../../../../../core/public';
+import { KibanaThemeProvider } from '../../../../kibana_react/public';
+import { ExpressionRenderDefinition } from '../../../../expressions/common/expression_renderers';
+import {
+ EXPRESSION_HEATMAP_NAME,
+ HeatmapExpressionProps,
+ FilterEvent,
+ BrushEvent,
+} from '../../common';
+import { getFormatService, getPaletteService, getUISettings, getThemeService } from '../services';
+import { getTimeZone } from '../utils/get_timezone';
+
+import HeatmapComponent from '../components/heatmap_component';
+import './index.scss';
+const MemoizedChart = memo(HeatmapComponent);
+
+interface ExpressioHeatmapRendererDependencies {
+ theme: ThemeServiceStart;
+}
+
+export const heatmapRenderer: (
+ deps: ExpressioHeatmapRendererDependencies
+) => ExpressionRenderDefinition = ({ theme }) => ({
+ name: EXPRESSION_HEATMAP_NAME,
+ displayName: i18n.translate('expressionHeatmap.visualizationName', {
+ defaultMessage: 'Heatmap',
+ }),
+ reuseDomNode: true,
+ render: async (domNode, config, handlers) => {
+ handlers.onDestroy(() => {
+ unmountComponentAtNode(domNode);
+ });
+ const onClickValue = (data: FilterEvent['data']) => {
+ handlers.event({ name: 'filter', data });
+ };
+ const onSelectRange = (data: BrushEvent['data']) => {
+ handlers.event({ name: 'brush', data });
+ };
+
+ const timeZone = getTimeZone(getUISettings());
+ render(
+
+
+
+
+ ,
+ domNode,
+ () => {
+ handlers.done();
+ }
+ );
+ },
+});
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss
new file mode 100644
index 0000000000000..6e1afd91c476d
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss
@@ -0,0 +1,26 @@
+.heatmap-container {
+ @include euiScrollBar;
+ height: 100%;
+ width: 100%;
+ // the FocusTrap is adding extra divs which are making the visualization redraw twice
+ // with a visible glitch. This make the chart library resilient to this extra reflow
+ overflow: auto hidden;
+ user-select: text;
+ padding: $euiSizeS;
+}
+
+.heatmap-chart__empty {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.heatmap-chart-icon__subdued {
+ fill: $euiTextSubduedColor;
+}
+
+.heatmap-chart-icon__accent {
+ fill: $euiColorVis0;
+}
\ No newline at end of file
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.ts b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.ts
new file mode 100644
index 0000000000000..d74f77b4eb5fc
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { heatmapRenderer } from './heatmap_renderer';
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/index.ts b/src/plugins/chart_expressions/expression_heatmap/public/index.ts
new file mode 100644
index 0000000000000..fbbf8027eb343
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ExpressionHeatmapPlugin } from './plugin';
+
+export function plugin() {
+ return new ExpressionHeatmapPlugin();
+}
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/plugin.ts b/src/plugins/chart_expressions/expression_heatmap/public/plugin.ts
new file mode 100644
index 0000000000000..cabb938f6f6b4
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/plugin.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { ChartsPluginSetup } from '../../../charts/public';
+import { CoreSetup, CoreStart } from '../../../../core/public';
+import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public';
+import { heatmapFunction, heatmapLegendConfig, heatmapGridConfig } from '../common';
+import { setFormatService, setPaletteService, setUISettings, setThemeService } from './services';
+import { heatmapRenderer } from './expression_renderers';
+import type { FieldFormatsStart } from '../../../field_formats/public';
+
+/** @internal */
+export interface ExpressionHeatmapPluginSetup {
+ expressions: ReturnType;
+ charts: ChartsPluginSetup;
+}
+
+/** @internal */
+export interface ExpressionHeatmapPluginStart {
+ fieldFormats: FieldFormatsStart;
+}
+
+/** @internal */
+export class ExpressionHeatmapPlugin {
+ public setup(core: CoreSetup, { expressions, charts }: ExpressionHeatmapPluginSetup) {
+ charts.palettes.getPalettes().then((palettes) => {
+ setPaletteService(palettes);
+ });
+ setUISettings(core.uiSettings);
+ setThemeService(charts.theme);
+ expressions.registerFunction(heatmapFunction);
+ expressions.registerFunction(heatmapLegendConfig);
+ expressions.registerFunction(heatmapGridConfig);
+ expressions.registerRenderer(heatmapRenderer({ theme: core.theme }));
+ }
+
+ public start(core: CoreStart, { fieldFormats }: ExpressionHeatmapPluginStart) {
+ setFormatService(fieldFormats);
+ }
+}
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/services/format_service.ts b/src/plugins/chart_expressions/expression_heatmap/public/services/format_service.ts
new file mode 100644
index 0000000000000..73b66341c4d9a
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/services/format_service.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createGetterSetter } from '../../../../kibana_utils/public';
+import { FieldFormatsStart } from '../../../../field_formats/public';
+
+export const [getFormatService, setFormatService] =
+ createGetterSetter('fieldFormats');
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/services/index.ts b/src/plugins/chart_expressions/expression_heatmap/public/services/index.ts
new file mode 100644
index 0000000000000..a86d9e4fee7c2
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/services/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { getFormatService, setFormatService } from './format_service';
+export {
+ getPaletteService,
+ setPaletteService,
+ setThemeService,
+ getThemeService,
+} from './palette_service';
+export { getUISettings, setUISettings } from './ui_settings';
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/services/palette_service.ts b/src/plugins/chart_expressions/expression_heatmap/public/services/palette_service.ts
new file mode 100644
index 0000000000000..4e76e3149c7a6
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/services/palette_service.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createGetterSetter } from '../../../../kibana_utils/public';
+import { PaletteRegistry, ChartsPluginSetup } from '../../../../charts/public';
+
+export const [getPaletteService, setPaletteService] =
+ createGetterSetter('palette');
+
+export const [getThemeService, setThemeService] =
+ createGetterSetter('charts.theme');
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/services/ui_settings.ts b/src/plugins/chart_expressions/expression_heatmap/public/services/ui_settings.ts
new file mode 100644
index 0000000000000..5e49e8da28840
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/services/ui_settings.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createGetterSetter } from '../../../../kibana_utils/public';
+import { CoreSetup } from '../../../../../core/public';
+
+export const [getUISettings, setUISettings] =
+ createGetterSetter('core.uiSettings');
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/utils/get_color_picker.tsx b/src/plugins/chart_expressions/expression_heatmap/public/utils/get_color_picker.tsx
new file mode 100644
index 0000000000000..2f5297c5fd475
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/utils/get_color_picker.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useCallback } from 'react';
+import { LegendColorPicker, Position } from '@elastic/charts';
+import { PopoverAnchorPosition, EuiPopover, EuiOutsideClickDetector } from '@elastic/eui';
+import type { PersistedState } from '../../../../visualizations/public';
+import { ColorPicker } from '../../../../charts/public';
+
+const KEY_CODE_ENTER = 13;
+
+function getAnchorPosition(legendPosition: Position): PopoverAnchorPosition {
+ switch (legendPosition) {
+ case Position.Bottom:
+ return 'upCenter';
+ case Position.Top:
+ return 'downCenter';
+ case Position.Left:
+ return 'rightCenter';
+ default:
+ return 'leftCenter';
+ }
+}
+
+export const getColorPicker =
+ (
+ legendPosition: Position,
+ setColor: (newColor: string | null, seriesKey: string | number) => void,
+ uiState: PersistedState
+ ): LegendColorPicker =>
+ ({ anchor, color, onClose, onChange, seriesIdentifiers: [seriesIdentifier] }) => {
+ const seriesName = seriesIdentifier.key;
+ const overwriteColors: Record = uiState?.get('vis.colors', {}) ?? {};
+ const colorIsOverwritten = seriesName.toString() in overwriteColors;
+ let keyDownEventOn = false;
+ const handleChange = (newColor: string | null) => {
+ if (newColor) {
+ onChange(newColor);
+ }
+ setColor(newColor, seriesName);
+ // close the popover if no color is applied or the user has clicked a color
+ if (!newColor || !keyDownEventOn) {
+ onClose();
+ }
+ };
+
+ const onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.keyCode === KEY_CODE_ENTER) {
+ onClose?.();
+ }
+ keyDownEventOn = true;
+ };
+
+ const handleOutsideClick = useCallback(() => {
+ onClose?.();
+ }, [onClose]);
+
+ return (
+
+
+
+
+
+ );
+ };
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/utils/get_timezone.ts b/src/plugins/chart_expressions/expression_heatmap/public/utils/get_timezone.ts
new file mode 100644
index 0000000000000..8d33a94c956d0
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/public/utils/get_timezone.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import moment from 'moment';
+import type { IUiSettingsClient } from '../../../../../core/public';
+
+/**
+ * Get timeZone from uiSettings
+ */
+export function getTimeZone(uiSettings: IUiSettingsClient) {
+ if (uiSettings.isDefault('dateFormat:tz')) {
+ const detectedTimeZone = moment.tz.guess();
+ return detectedTimeZone || moment().format('Z');
+ } else {
+ return uiSettings.get('dateFormat:tz', 'Browser');
+ }
+}
diff --git a/src/plugins/chart_expressions/expression_heatmap/server/index.ts b/src/plugins/chart_expressions/expression_heatmap/server/index.ts
new file mode 100644
index 0000000000000..fbbf8027eb343
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/server/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ExpressionHeatmapPlugin } from './plugin';
+
+export function plugin() {
+ return new ExpressionHeatmapPlugin();
+}
diff --git a/src/plugins/chart_expressions/expression_heatmap/server/plugin.ts b/src/plugins/chart_expressions/expression_heatmap/server/plugin.ts
new file mode 100644
index 0000000000000..858c67da86a6e
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/server/plugin.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { CoreSetup, CoreStart, Plugin } from '../../../../core/public';
+import { ExpressionsServerStart, ExpressionsServerSetup } from '../../../expressions/server';
+import { heatmapFunction, heatmapLegendConfig, heatmapGridConfig } from '../common';
+
+interface SetupDeps {
+ expressions: ExpressionsServerSetup;
+}
+
+interface StartDeps {
+ expression: ExpressionsServerStart;
+}
+
+export type ExpressionHeatmapPluginSetup = void;
+export type ExpressionHeatmapPluginStart = void;
+
+export class ExpressionHeatmapPlugin
+ implements
+ Plugin
+{
+ public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionHeatmapPluginSetup {
+ expressions.registerFunction(heatmapFunction);
+ expressions.registerFunction(heatmapLegendConfig);
+ expressions.registerFunction(heatmapGridConfig);
+ }
+
+ public start(core: CoreStart): ExpressionHeatmapPluginStart {}
+
+ public stop() {}
+}
diff --git a/src/plugins/chart_expressions/expression_heatmap/tsconfig.json b/src/plugins/chart_expressions/expression_heatmap/tsconfig.json
new file mode 100644
index 0000000000000..ff5089c7f4d21
--- /dev/null
+++ b/src/plugins/chart_expressions/expression_heatmap/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true,
+ "isolatedModules": true
+ },
+ "include": [
+ "common/**/*",
+ "public/**/*",
+ "server/**/*",
+ ],
+ "references": [
+ { "path": "../../../core/tsconfig.json" },
+ { "path": "../../expressions/tsconfig.json" },
+ { "path": "../../presentation_util/tsconfig.json" },
+ { "path": "../../field_formats/tsconfig.json" },
+ { "path": "../../charts/tsconfig.json" },
+ { "path": "../../visualizations/tsconfig.json" },
+ ]
+}
diff --git a/src/plugins/charts/public/static/components/empty_placeholder.tsx b/src/plugins/charts/public/static/components/empty_placeholder.tsx
new file mode 100644
index 0000000000000..db3f3fb6739d5
--- /dev/null
+++ b/src/plugins/charts/public/static/components/empty_placeholder.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+export const EmptyPlaceholder = (props: { icon: IconType }) => (
+ <>
+
+
+
+
+
+
+
+ >
+);
diff --git a/src/plugins/charts/public/static/components/index.ts b/src/plugins/charts/public/static/components/index.ts
index 7f3af50a01aa4..ea3e66e7f30a3 100644
--- a/src/plugins/charts/public/static/components/index.ts
+++ b/src/plugins/charts/public/static/components/index.ts
@@ -9,4 +9,5 @@
export { LegendToggle } from './legend_toggle';
export { ColorPicker } from './color_picker';
export { CurrentTime } from './current_time';
+export { EmptyPlaceholder } from './empty_placeholder';
export * from './endzones';
diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts
index 4cb091787c058..7f9ce07efbe51 100644
--- a/src/plugins/data/common/search/aggs/agg_config.ts
+++ b/src/plugins/data/common/search/aggs/agg_config.ts
@@ -402,6 +402,10 @@ export class AggConfig {
return this.type.getValue(this, bucket);
}
+ getResponseId() {
+ return this.type.getResponseId(this);
+ }
+
getKey(bucket: any, key?: string) {
if (this.type.getKey) {
return this.type.getKey(bucket, key, this);
diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts
index 104cd3b2815bc..80e5a079cfd59 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.test.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts
@@ -360,6 +360,7 @@ describe('AggConfigs', () => {
"0": Object {
"range": Object {
"@timestamp": Object {
+ "format": "strict_date_optional_time",
"gte": "2021-05-05T00:00:00.000Z",
"lte": "2021-05-10T00:00:00.000Z",
},
@@ -368,6 +369,7 @@ describe('AggConfigs', () => {
"86400000": Object {
"range": Object {
"@timestamp": Object {
+ "format": "strict_date_optional_time",
"gte": "2021-05-04T00:00:00.000Z",
"lte": "2021-05-09T00:00:00.000Z",
},
diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts
index 9a362466c0fd7..a022abba7fb45 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.ts
@@ -406,6 +406,7 @@ export class AggConfigs {
.map(([filter, field]) => ({
range: {
[field]: {
+ format: 'strict_date_optional_time',
gte: moment(filter?.query.range[field].gte).subtract(shift).toISOString(),
lte: moment(filter?.query.range[field].lte).subtract(shift).toISOString(),
},
diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts
index 3f91eadd19eb4..b22780273c23e 100644
--- a/src/plugins/data/common/search/aggs/agg_type.ts
+++ b/src/plugins/data/common/search/aggs/agg_type.ts
@@ -58,6 +58,7 @@ export interface AggTypeConfig<
getValue?: (agg: TAggConfig, bucket: any) => any;
getKey?: (bucket: any, key: any, agg: TAggConfig) => any;
getValueBucketPath?: (agg: TAggConfig) => string;
+ getResponseId?: (agg: TAggConfig) => string;
}
// TODO need to make a more explicit interface for this
@@ -224,6 +225,25 @@ export class AggType<
return false;
}
+ /**
+ * Returns the key of the object containing the results of the agg in the Elasticsearch response object.
+ * In most cases this returns the `agg.id` property, but in some cases the response object is structured differently.
+ * In the following example of a terms agg, `getResponseId` returns "myAgg":
+ * ```
+ * {
+ * "aggregations": {
+ * "myAgg": {
+ * "doc_count_error_upper_bound": 0,
+ * "sum_other_doc_count": 0,
+ * "buckets": [
+ * ...
+ * ```
+ *
+ * @param {agg} agg - the agg to return the id in the ES reponse object for
+ * @return {string}
+ */
+ getResponseId: (agg: TAggConfig) => string;
+
/**
* Generic AggType Constructor
*
@@ -298,5 +318,7 @@ export class AggType<
});
this.getValue = config.getValue || ((agg: TAggConfig, bucket: any) => {});
+
+ this.getResponseId = config.getResponseId || ((agg: TAggConfig) => agg.id);
}
}
diff --git a/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts b/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts
index b27e4dd1494be..c86c5d89ff6a3 100644
--- a/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts
+++ b/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts
@@ -69,4 +69,10 @@ describe('filtered metric agg type', () => {
})
).toEqual(10);
});
+
+ it('provides the id of the inner filter bucket to look up the agg config in the response object', () => {
+ const agg = aggConfigs.getResponseAggs()[0];
+
+ expect(agg.getResponseId()).toEqual('filtered_metric-bucket');
+ });
});
diff --git a/src/plugins/data/common/search/aggs/metrics/filtered_metric.ts b/src/plugins/data/common/search/aggs/metrics/filtered_metric.ts
index 00f47d31b0398..fe35f3ea90008 100644
--- a/src/plugins/data/common/search/aggs/metrics/filtered_metric.ts
+++ b/src/plugins/data/common/search/aggs/metrics/filtered_metric.ts
@@ -52,5 +52,8 @@ export const getFilteredMetricAgg = () => {
}
return `${customBucket.getValueBucketPath()}>${customMetric.getValueBucketPath()}`;
},
+ getResponseId(agg) {
+ return agg.params.customBucket.id;
+ },
});
};
diff --git a/src/plugins/data/common/search/aggs/utils/time_splits.ts b/src/plugins/data/common/search/aggs/utils/time_splits.ts
index c4a603a383e38..6eb1827e56b13 100644
--- a/src/plugins/data/common/search/aggs/utils/time_splits.ts
+++ b/src/plugins/data/common/search/aggs/utils/time_splits.ts
@@ -186,7 +186,7 @@ export function mergeTimeShifts(
return;
} else {
// a sub-agg
- const agg = requestAggs.find((requestAgg) => key.indexOf(requestAgg.id) === 0);
+ const agg = requestAggs.find((requestAgg) => key === requestAgg.getResponseId());
if (agg && agg.type.type === AggGroupNames.Metrics) {
const timeShift = agg.getTimeShift();
if (
@@ -430,6 +430,7 @@ export function insertTimeShiftSplit(
filters[key] = {
range: {
[timeField]: {
+ format: 'strict_date_optional_time',
gte: moment(timeFilter.query.range[timeField].gte).subtract(shift).toISOString(),
lte: moment(timeFilter.query.range[timeField].lte).subtract(shift).toISOString(),
},
diff --git a/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap b/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap
index af9499bd7e263..50054478173e6 100644
--- a/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap
+++ b/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap
@@ -22,6 +22,10 @@ FldList [
]
`;
+exports[`IndexPatterns createAndSave will throw if insufficient access 1`] = `[DataViewInsufficientAccessError: Operation failed due to insufficient access, id: undefined]`;
+
+exports[`IndexPatterns delete will throw if insufficient access 1`] = `[DataViewInsufficientAccessError: Operation failed due to insufficient access, id: 1]`;
+
exports[`IndexPatterns savedObjectToSpec 1`] = `
Object {
"allowNoIndex": undefined,
@@ -60,3 +64,5 @@ Object {
"version": "version",
}
`;
+
+exports[`IndexPatterns updateSavedObject will throw if insufficient access 1`] = `[DataViewInsufficientAccessError: Operation failed due to insufficient access, id: id]`;
diff --git a/src/plugins/data_views/common/data_views/data_views.test.ts b/src/plugins/data_views/common/data_views/data_views.test.ts
index 210c926f92df7..7ed3e1f2c5434 100644
--- a/src/plugins/data_views/common/data_views/data_views.test.ts
+++ b/src/plugins/data_views/common/data_views/data_views.test.ts
@@ -48,6 +48,7 @@ const savedObject = {
describe('IndexPatterns', () => {
let indexPatterns: DataViewsService;
+ let indexPatternsNoAccess: DataViewsService;
let savedObjectsClient: SavedObjectsClientCommon;
let SOClientGetDelay = 0;
const uiSettings = {
@@ -99,6 +100,18 @@ describe('IndexPatterns', () => {
onNotification: () => {},
onError: () => {},
onRedirectNoIndexPattern: () => {},
+ getCanSave: () => Promise.resolve(true),
+ });
+
+ indexPatternsNoAccess = new DataViewsService({
+ uiSettings,
+ savedObjectsClient: savedObjectsClient as unknown as SavedObjectsClientCommon,
+ apiClient: createFieldsFetcher(),
+ fieldFormats,
+ onNotification: () => {},
+ onError: () => {},
+ onRedirectNoIndexPattern: () => {},
+ getCanSave: () => Promise.resolve(false),
});
});
@@ -171,6 +184,10 @@ describe('IndexPatterns', () => {
expect(indexPattern).not.toBe(await indexPatterns.get(id));
});
+ test('delete will throw if insufficient access', async () => {
+ await expect(indexPatternsNoAccess.delete('1')).rejects.toMatchSnapshot();
+ });
+
test('should handle version conflicts', async () => {
setDocsourcePayload(null, {
id: 'foo',
@@ -246,6 +263,18 @@ describe('IndexPatterns', () => {
expect(indexPatterns.setDefault).toBeCalled();
});
+ test('createAndSave will throw if insufficient access', async () => {
+ const title = 'kibana-*';
+
+ await expect(indexPatternsNoAccess.createAndSave({ title })).rejects.toMatchSnapshot();
+ });
+
+ test('updateSavedObject will throw if insufficient access', async () => {
+ await expect(
+ indexPatternsNoAccess.updateSavedObject({ id: 'id' } as unknown as DataView)
+ ).rejects.toMatchSnapshot();
+ });
+
test('savedObjectToSpec', () => {
const spec = indexPatterns.savedObjectToSpec(savedObject);
expect(spec).toMatchSnapshot();
diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts
index b54bcf3471d4b..bb62bd1875aae 100644
--- a/src/plugins/data_views/common/data_views/data_views.ts
+++ b/src/plugins/data_views/common/data_views/data_views.ts
@@ -35,7 +35,7 @@ import { META_FIELDS, SavedObject } from '../../common';
import { SavedObjectNotFound } from '../../../kibana_utils/common';
import { DataViewMissingIndices } from '../lib';
import { findByTitle } from '../utils';
-import { DuplicateDataViewError } from '../errors';
+import { DuplicateDataViewError, DataViewInsufficientAccessError } from '../errors';
const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3;
@@ -67,6 +67,7 @@ interface IndexPatternsServiceDeps {
onNotification: OnNotification;
onError: OnError;
onRedirectNoIndexPattern?: () => void;
+ getCanSave: () => Promise;
}
export class DataViewsService {
@@ -78,6 +79,7 @@ export class DataViewsService {
private onNotification: OnNotification;
private onError: OnError;
private dataViewCache: ReturnType;
+ private getCanSave: () => Promise;
/**
* @deprecated Use `getDefaultDataView` instead (when loading data view) and handle
@@ -93,6 +95,7 @@ export class DataViewsService {
onNotification,
onError,
onRedirectNoIndexPattern = () => {},
+ getCanSave = () => Promise.resolve(false),
}: IndexPatternsServiceDeps) {
this.apiClient = apiClient;
this.config = uiSettings;
@@ -101,6 +104,7 @@ export class DataViewsService {
this.onNotification = onNotification;
this.onError = onError;
this.ensureDefaultDataView = createEnsureDefaultDataView(uiSettings, onRedirectNoIndexPattern);
+ this.getCanSave = getCanSave;
this.dataViewCache = createDataViewCache();
}
@@ -557,6 +561,9 @@ export class DataViewsService {
*/
async createSavedObject(indexPattern: DataView, override = false) {
+ if (!(await this.getCanSave())) {
+ throw new DataViewInsufficientAccessError();
+ }
const dupe = await findByTitle(this.savedObjectsClient, indexPattern.title);
if (dupe) {
if (override) {
@@ -595,6 +602,9 @@ export class DataViewsService {
ignoreErrors: boolean = false
): Promise {
if (!indexPattern.id) return;
+ if (!(await this.getCanSave())) {
+ throw new DataViewInsufficientAccessError(indexPattern.id);
+ }
// get the list of attributes
const body = indexPattern.getAsSavedObjectBody();
@@ -678,6 +688,9 @@ export class DataViewsService {
* @param indexPatternId: Id of kibana Index Pattern to delete
*/
async delete(indexPatternId: string) {
+ if (!(await this.getCanSave())) {
+ throw new DataViewInsufficientAccessError(indexPatternId);
+ }
this.dataViewCache.clear(indexPatternId);
return this.savedObjectsClient.delete(DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId);
}
diff --git a/src/plugins/data_views/common/errors/index.ts b/src/plugins/data_views/common/errors/index.ts
index 20ff90d3fd6cf..97203b2a4baeb 100644
--- a/src/plugins/data_views/common/errors/index.ts
+++ b/src/plugins/data_views/common/errors/index.ts
@@ -8,3 +8,4 @@
export * from './duplicate_index_pattern';
export * from './data_view_saved_object_conflict';
+export * from './insufficient_access';
diff --git a/src/plugins/data_views/common/errors/insufficient_access.ts b/src/plugins/data_views/common/errors/insufficient_access.ts
new file mode 100644
index 0000000000000..48c826ec78557
--- /dev/null
+++ b/src/plugins/data_views/common/errors/insufficient_access.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export class DataViewInsufficientAccessError extends Error {
+ constructor(savedObjectId?: string) {
+ super(`Operation failed due to insufficient access, id: ${savedObjectId}`);
+ this.name = 'DataViewInsufficientAccessError';
+ }
+}
diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts
index 58f66623b64ab..4a00ea91a47bd 100644
--- a/src/plugins/data_views/public/plugin.ts
+++ b/src/plugins/data_views/public/plugin.ts
@@ -61,6 +61,7 @@ export class DataViewsPublicPlugin
application.navigateToApp,
overlays
),
+ getCanSave: () => Promise.resolve(application.capabilities.indexPatterns.save === true),
});
}
diff --git a/src/plugins/data_views/server/data_views_service_factory.ts b/src/plugins/data_views/server/data_views_service_factory.ts
index 2f720cd7388f4..620107231fb4f 100644
--- a/src/plugins/data_views/server/data_views_service_factory.ts
+++ b/src/plugins/data_views/server/data_views_service_factory.ts
@@ -11,6 +11,8 @@ import {
SavedObjectsClientContract,
ElasticsearchClient,
UiSettingsServiceStart,
+ KibanaRequest,
+ CoreStart,
} from 'kibana/server';
import { DataViewsService } from '../common';
import { FieldFormatsStart } from '../../field_formats/server';
@@ -23,14 +25,17 @@ export const dataViewsServiceFactory =
logger,
uiSettings,
fieldFormats,
+ capabilities,
}: {
logger: Logger;
uiSettings: UiSettingsServiceStart;
fieldFormats: FieldFormatsStart;
+ capabilities: CoreStart['capabilities'];
}) =>
async (
savedObjectsClient: SavedObjectsClientContract,
- elasticsearchClient: ElasticsearchClient
+ elasticsearchClient: ElasticsearchClient,
+ request?: KibanaRequest
) => {
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient);
@@ -46,5 +51,9 @@ export const dataViewsServiceFactory =
onNotification: ({ title, text }) => {
logger.warn(`${title}${text ? ` : ${text}` : ''}`);
},
+ getCanSave: async () =>
+ request
+ ? (await capabilities.resolveCapabilities(request)).indexPatterns.save === true
+ : false,
});
};
diff --git a/src/plugins/data_views/server/expressions/load_index_pattern.ts b/src/plugins/data_views/server/expressions/load_index_pattern.ts
index 8ade41132e144..8604709e81582 100644
--- a/src/plugins/data_views/server/expressions/load_index_pattern.ts
+++ b/src/plugins/data_views/server/expressions/load_index_pattern.ts
@@ -85,7 +85,8 @@ export function getIndexPatternLoad({
return {
indexPatterns: await indexPatternsServiceFactory(
savedObjects.getScopedClient(request),
- elasticsearch.client.asScoped(request).asCurrentUser
+ elasticsearch.client.asScoped(request).asCurrentUser,
+ request
),
};
},
diff --git a/src/plugins/data_views/server/plugin.ts b/src/plugins/data_views/server/plugin.ts
index 7285e74847e58..6b9e28101ac24 100644
--- a/src/plugins/data_views/server/plugin.ts
+++ b/src/plugins/data_views/server/plugin.ts
@@ -53,13 +53,14 @@ export class DataViewsServerPlugin
}
public start(
- { uiSettings }: CoreStart,
+ { uiSettings, capabilities }: CoreStart,
{ fieldFormats }: DataViewsServerPluginStartDependencies
) {
const serviceFactory = dataViewsServiceFactory({
logger: this.logger.get('indexPatterns'),
uiSettings,
fieldFormats,
+ capabilities,
});
return {
diff --git a/src/plugins/data_views/server/routes/create_index_pattern.ts b/src/plugins/data_views/server/routes/create_index_pattern.ts
index b87b03f8bd4a1..d50012596ee56 100644
--- a/src/plugins/data_views/server/routes/create_index_pattern.ts
+++ b/src/plugins/data_views/server/routes/create_index_pattern.ts
@@ -71,7 +71,8 @@ export const registerCreateIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const body = req.body;
diff --git a/src/plugins/data_views/server/routes/default_index_pattern.ts b/src/plugins/data_views/server/routes/default_index_pattern.ts
index 620e201a4850d..1fe56db6c7488 100644
--- a/src/plugins/data_views/server/routes/default_index_pattern.ts
+++ b/src/plugins/data_views/server/routes/default_index_pattern.ts
@@ -29,7 +29,8 @@ export const registerManageDefaultIndexPatternRoutes = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const defaultIndexPatternId = await indexPatternsService.getDefaultId();
@@ -63,7 +64,8 @@ export const registerManageDefaultIndexPatternRoutes = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const newDefaultId = req.body.index_pattern_id;
diff --git a/src/plugins/data_views/server/routes/delete_index_pattern.ts b/src/plugins/data_views/server/routes/delete_index_pattern.ts
index 0d3f929cdccc3..151fb0b0224b6 100644
--- a/src/plugins/data_views/server/routes/delete_index_pattern.ts
+++ b/src/plugins/data_views/server/routes/delete_index_pattern.ts
@@ -40,7 +40,8 @@ export const registerDeleteIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
diff --git a/src/plugins/data_views/server/routes/fields/update_fields.ts b/src/plugins/data_views/server/routes/fields/update_fields.ts
index 3e45ee46f2bb7..258ae9ebec3af 100644
--- a/src/plugins/data_views/server/routes/fields/update_fields.ts
+++ b/src/plugins/data_views/server/routes/fields/update_fields.ts
@@ -64,7 +64,8 @@ export const registerUpdateFieldsRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const { fields } = req.body;
diff --git a/src/plugins/data_views/server/routes/get_index_pattern.ts b/src/plugins/data_views/server/routes/get_index_pattern.ts
index 7fea748ca3389..b7d95fe687a0a 100644
--- a/src/plugins/data_views/server/routes/get_index_pattern.ts
+++ b/src/plugins/data_views/server/routes/get_index_pattern.ts
@@ -40,7 +40,8 @@ export const registerGetIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const indexPattern = await indexPatternsService.get(id);
diff --git a/src/plugins/data_views/server/routes/has_user_index_pattern.ts b/src/plugins/data_views/server/routes/has_user_index_pattern.ts
index af0ad1cc88d2e..6562d06df6f65 100644
--- a/src/plugins/data_views/server/routes/has_user_index_pattern.ts
+++ b/src/plugins/data_views/server/routes/has_user_index_pattern.ts
@@ -29,7 +29,8 @@ export const registerHasUserIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
return res.ok({
diff --git a/src/plugins/data_views/server/routes/runtime_fields/create_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/create_runtime_field.ts
index 04b661d14732f..434d57f1aeecb 100644
--- a/src/plugins/data_views/server/routes/runtime_fields/create_runtime_field.ts
+++ b/src/plugins/data_views/server/routes/runtime_fields/create_runtime_field.ts
@@ -48,7 +48,8 @@ export const registerCreateRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const { name, runtimeField } = req.body;
diff --git a/src/plugins/data_views/server/routes/runtime_fields/delete_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/delete_runtime_field.ts
index e5c6b03a64224..d15365647f2a0 100644
--- a/src/plugins/data_views/server/routes/runtime_fields/delete_runtime_field.ts
+++ b/src/plugins/data_views/server/routes/runtime_fields/delete_runtime_field.ts
@@ -44,7 +44,8 @@ export const registerDeleteRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const name = req.params.name;
diff --git a/src/plugins/data_views/server/routes/runtime_fields/get_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/get_runtime_field.ts
index b457ae6b0159b..a6f45b81af149 100644
--- a/src/plugins/data_views/server/routes/runtime_fields/get_runtime_field.ts
+++ b/src/plugins/data_views/server/routes/runtime_fields/get_runtime_field.ts
@@ -45,7 +45,8 @@ export const registerGetRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const name = req.params.name;
diff --git a/src/plugins/data_views/server/routes/runtime_fields/put_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/put_runtime_field.ts
index 1c3ed99fdf67e..7cea9864f17dd 100644
--- a/src/plugins/data_views/server/routes/runtime_fields/put_runtime_field.ts
+++ b/src/plugins/data_views/server/routes/runtime_fields/put_runtime_field.ts
@@ -47,7 +47,8 @@ export const registerPutRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const { name, runtimeField } = req.body;
diff --git a/src/plugins/data_views/server/routes/runtime_fields/update_runtime_field.ts b/src/plugins/data_views/server/routes/runtime_fields/update_runtime_field.ts
index ca92f310ff281..b2c6bf0576b9b 100644
--- a/src/plugins/data_views/server/routes/runtime_fields/update_runtime_field.ts
+++ b/src/plugins/data_views/server/routes/runtime_fields/update_runtime_field.ts
@@ -55,7 +55,8 @@ export const registerUpdateRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const name = req.params.name;
diff --git a/src/plugins/data_views/server/routes/scripted_fields/create_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/create_scripted_field.ts
index e620960afbe13..b3660f5841442 100644
--- a/src/plugins/data_views/server/routes/scripted_fields/create_scripted_field.ts
+++ b/src/plugins/data_views/server/routes/scripted_fields/create_scripted_field.ts
@@ -47,7 +47,8 @@ export const registerCreateScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const { field } = req.body;
diff --git a/src/plugins/data_views/server/routes/scripted_fields/delete_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/delete_scripted_field.ts
index bd1bfe0ec4e25..628ba9fb41aa1 100644
--- a/src/plugins/data_views/server/routes/scripted_fields/delete_scripted_field.ts
+++ b/src/plugins/data_views/server/routes/scripted_fields/delete_scripted_field.ts
@@ -48,7 +48,8 @@ export const registerDeleteScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const name = req.params.name;
diff --git a/src/plugins/data_views/server/routes/scripted_fields/get_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/get_scripted_field.ts
index ae9cca2c79b48..82f14faaaed99 100644
--- a/src/plugins/data_views/server/routes/scripted_fields/get_scripted_field.ts
+++ b/src/plugins/data_views/server/routes/scripted_fields/get_scripted_field.ts
@@ -48,7 +48,8 @@ export const registerGetScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const name = req.params.name;
diff --git a/src/plugins/data_views/server/routes/scripted_fields/put_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/put_scripted_field.ts
index a6cee3762513e..bd79203b1161f 100644
--- a/src/plugins/data_views/server/routes/scripted_fields/put_scripted_field.ts
+++ b/src/plugins/data_views/server/routes/scripted_fields/put_scripted_field.ts
@@ -47,7 +47,8 @@ export const registerPutScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const { field } = req.body;
diff --git a/src/plugins/data_views/server/routes/scripted_fields/update_scripted_field.ts b/src/plugins/data_views/server/routes/scripted_fields/update_scripted_field.ts
index 2917838293ec8..fba7488a83a24 100644
--- a/src/plugins/data_views/server/routes/scripted_fields/update_scripted_field.ts
+++ b/src/plugins/data_views/server/routes/scripted_fields/update_scripted_field.ts
@@ -68,7 +68,8 @@ export const registerUpdateScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
const name = req.params.name;
diff --git a/src/plugins/data_views/server/routes/update_index_pattern.ts b/src/plugins/data_views/server/routes/update_index_pattern.ts
index 1421057d65d26..078ef5dec5de5 100644
--- a/src/plugins/data_views/server/routes/update_index_pattern.ts
+++ b/src/plugins/data_views/server/routes/update_index_pattern.ts
@@ -68,7 +68,8 @@ export const registerUpdateIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
- elasticsearchClient
+ elasticsearchClient,
+ req
);
const id = req.params.id;
diff --git a/src/plugins/data_views/server/types.ts b/src/plugins/data_views/server/types.ts
index 4a57a1d01b9c3..a5111a535f3ef 100644
--- a/src/plugins/data_views/server/types.ts
+++ b/src/plugins/data_views/server/types.ts
@@ -6,7 +6,12 @@
* Side Public License, v 1.
*/
-import { Logger, SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server';
+import {
+ Logger,
+ SavedObjectsClientContract,
+ ElasticsearchClient,
+ KibanaRequest,
+} from 'kibana/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { DataViewsService } from '../common';
@@ -14,7 +19,8 @@ import { FieldFormatsSetup, FieldFormatsStart } from '../../field_formats/server
type ServiceFactory = (
savedObjectsClient: SavedObjectsClientContract,
- elasticsearchClient: ElasticsearchClient
+ elasticsearchClient: ElasticsearchClient,
+ request?: KibanaRequest
) => Promise;
export interface DataViewsServerPluginStart {
dataViewsServiceFactory: ServiceFactory;
diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap
index d85f96382e803..1cf6a3409539e 100644
--- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap
+++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap
@@ -199,6 +199,7 @@ exports[` is rendered 1`] = `
"renderLineHighlight": "none",
"scrollBeyondLastLine": false,
"scrollbar": Object {
+ "alwaysConsumeMouseWheel": false,
"useShadows": false,
},
"wordBasedSuggestions": false,
diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx
index 93cee7c0477e7..754cc47434479 100644
--- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx
+++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx
@@ -397,6 +397,10 @@ export const CodeEditor: React.FC = ({
},
scrollbar: {
useShadows: false,
+ // Scroll events are handled only when there is scrollable content. When there is scrollable content, the
+ // editor should scroll to the bottom then break out of that scroll context and continue scrolling on any
+ // outer scrollbars.
+ alwaysConsumeMouseWheel: false,
},
wordBasedSuggestions: false,
wordWrap: 'on',
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
index 356aaf60b423c..c2a4f18218dd4 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -396,6 +396,10 @@ export const stackManagementSchema: MakeSchemaFrom = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
+ 'visualization:visualize:legacyHeatmapChartsLibrary': {
+ type: 'boolean',
+ _meta: { description: 'Non-default value of setting.' },
+ },
'doc_table:legacy': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
index 69287d37dfa28..69ed647f0845a 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -28,6 +28,7 @@ export interface UsageStats {
'autocomplete:valueSuggestionMethod': string;
'search:timeout': number;
'visualization:visualize:legacyPieChartsLibrary': boolean;
+ 'visualization:visualize:legacyHeatmapChartsLibrary': boolean;
'doc_table:legacy': boolean;
'discover:modifyColumnsOnSwitch': boolean;
'discover:searchFieldsFromSource': boolean;
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index cd5d3818bcdec..31bf9c3f08e71 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -7742,6 +7742,12 @@
"description": "Non-default value of setting."
}
},
+ "visualization:visualize:legacyHeatmapChartsLibrary": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Non-default value of setting."
+ }
+ },
"doc_table:legacy": {
"type": "boolean",
"_meta": {
diff --git a/src/plugins/vis_default_editor/public/components/options/color_schema.tsx b/src/plugins/vis_default_editor/public/components/options/color_schema.tsx
index 4e6fec5c98558..3ce9e2ec72fa0 100644
--- a/src/plugins/vis_default_editor/public/components/options/color_schema.tsx
+++ b/src/plugins/vis_default_editor/public/components/options/color_schema.tsx
@@ -51,6 +51,7 @@ function ColorSchemaOptions({
{
uiState.set('vis.colors', null);
+ uiState?.emit('reload');
setIsCustomColors(false);
}}
>
diff --git a/src/plugins/vis_default_editor/public/components/options/percentage_mode.tsx b/src/plugins/vis_default_editor/public/components/options/percentage_mode.tsx
index bfb6d2051452b..0a593dd753b53 100644
--- a/src/plugins/vis_default_editor/public/components/options/percentage_mode.tsx
+++ b/src/plugins/vis_default_editor/public/components/options/percentage_mode.tsx
@@ -22,6 +22,7 @@ export interface PercentageModeOptionProps {
percentageMode: boolean;
formatPattern?: string;
'data-test-subj'?: string;
+ disabled?: boolean;
}
function PercentageModeOption({
@@ -29,6 +30,7 @@ function PercentageModeOption({
setValue,
percentageMode,
formatPattern,
+ disabled,
}: PercentageModeOptionProps) {
const { services } = useKibana();
const defaultPattern = services.uiSettings?.get(
@@ -45,6 +47,7 @@ function PercentageModeOption({
paramName="percentageMode"
value={percentageMode}
setValue={setValue}
+ disabled={disabled}
/>
;
diff --git a/src/plugins/vis_types/heatmap/jest.config.js b/src/plugins/vis_types/heatmap/jest.config.js
new file mode 100644
index 0000000000000..6b9df821c48db
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/jest.config.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/src/plugins/vis_types/heatmap'],
+ coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/heatmap',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/src/plugins/vis_types/heatmap/{common,public,server}/**/*.{ts,tsx}',
+ ],
+};
diff --git a/src/plugins/vis_types/heatmap/kibana.json b/src/plugins/vis_types/heatmap/kibana.json
new file mode 100644
index 0000000000000..c8df98e2b343a
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/kibana.json
@@ -0,0 +1,14 @@
+{
+ "id": "visTypeHeatmap",
+ "version": "kibana",
+ "ui": true,
+ "server": true,
+ "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "fieldFormats"],
+ "requiredBundles": ["visDefaultEditor"],
+ "extraPublicDirs": ["common/index"],
+ "owner": {
+ "name": "Vis Editors",
+ "githubTeam": "kibana-vis-editors"
+ },
+ "description": "Contains the heatmap implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy heatmap charts library advanced setting."
+ }
diff --git a/src/plugins/vis_types/heatmap/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/heatmap/public/__snapshots__/to_ast.test.ts.snap
new file mode 100644
index 0000000000000..f7a8299920212
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/__snapshots__/to_ast.test.ts.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`heatmap vis toExpressionAst function should match basic snapshot 1`] = `
+Object {
+ "addArgument": [Function],
+ "arguments": Object {
+ "gridConfig": Array [
+ Object {
+ "toAst": [Function],
+ },
+ ],
+ "highlightInHover": Array [
+ false,
+ ],
+ "lastRangeIsRightOpen": Array [
+ false,
+ ],
+ "legend": Array [
+ Object {
+ "toAst": [Function],
+ },
+ ],
+ "palette": Array [
+ Object {
+ "toAst": [Function],
+ },
+ ],
+ "percentageMode": Array [
+ false,
+ ],
+ "showTooltip": Array [
+ true,
+ ],
+ "valueAccessor": Array [
+ Object {
+ "toAst": [Function],
+ },
+ ],
+ "xAccessor": Array [
+ Object {
+ "toAst": [Function],
+ },
+ ],
+ "yAccessor": Array [
+ Object {
+ "toAst": [Function],
+ },
+ ],
+ },
+ "getArgument": [Function],
+ "name": "heatmap",
+ "removeArgument": [Function],
+ "replaceArgument": [Function],
+ "toAst": [Function],
+ "toString": [Function],
+ "type": "expression_function_builder",
+}
+`;
diff --git a/src/plugins/vis_types/heatmap/public/editor/collections.ts b/src/plugins/vis_types/heatmap/public/editor/collections.ts
new file mode 100644
index 0000000000000..932a2c3205057
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/editor/collections.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { Position } from '@elastic/charts';
+import { ScaleType } from '../types';
+
+export const legendPositions = [
+ {
+ text: i18n.translate('visTypeHeatmap.legendPositions.topText', {
+ defaultMessage: 'Top',
+ }),
+ value: Position.Top,
+ },
+ {
+ text: i18n.translate('visTypeHeatmap.legendPositions.leftText', {
+ defaultMessage: 'Left',
+ }),
+ value: Position.Left,
+ },
+ {
+ text: i18n.translate('visTypeHeatmap.legendPositions.rightText', {
+ defaultMessage: 'Right',
+ }),
+ value: Position.Right,
+ },
+ {
+ text: i18n.translate('visTypeHeatmap.legendPositions.bottomText', {
+ defaultMessage: 'Bottom',
+ }),
+ value: Position.Bottom,
+ },
+];
+
+export const scaleTypes = [
+ {
+ text: i18n.translate('visTypeHeatmap.scaleTypes.linearText', {
+ defaultMessage: 'Linear',
+ }),
+ value: ScaleType.Linear,
+ },
+ {
+ text: i18n.translate('visTypeHeatmap.scaleTypes.logText', {
+ defaultMessage: 'Log',
+ }),
+ value: ScaleType.Log,
+ },
+ {
+ text: i18n.translate('visTypeHeatmap.scaleTypes.squareRootText', {
+ defaultMessage: 'Square root',
+ }),
+ value: ScaleType.SquareRoot,
+ },
+];
diff --git a/src/plugins/vis_types/heatmap/public/editor/components/heatmap.test.tsx b/src/plugins/vis_types/heatmap/public/editor/components/heatmap.test.tsx
new file mode 100644
index 0000000000000..5f57083072202
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/editor/components/heatmap.test.tsx
@@ -0,0 +1,205 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { mountWithIntl } from '@kbn/test/jest';
+import { ReactWrapper } from 'enzyme';
+import type { PersistedState } from '../../../../../visualizations/public';
+import HeatmapOptions, { HeatmapOptionsProps } from './heatmap';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { act } from 'react-dom/test-utils';
+
+describe('PalettePicker', function () {
+ let props: HeatmapOptionsProps;
+ let component: ReactWrapper;
+ const mockState = new Map();
+ const uiState = {
+ get: jest
+ .fn()
+ .mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)),
+ set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)),
+ emit: jest.fn(),
+ on: jest.fn(),
+ setSilent: jest.fn(),
+ } as unknown as PersistedState;
+
+ beforeAll(() => {
+ props = {
+ showElasticChartsOptions: true,
+ uiState,
+ setValidity: jest.fn(),
+ vis: {
+ type: {
+ editorConfig: {
+ collections: {
+ legendPositions: [
+ {
+ text: 'Top',
+ value: 'top',
+ },
+ {
+ text: 'Left',
+ value: 'left',
+ },
+ {
+ text: 'Right',
+ value: 'right',
+ },
+ {
+ text: 'Bottom',
+ value: 'bottom',
+ },
+ ],
+ },
+ },
+ },
+ },
+ stateParams: {
+ percentageMode: false,
+ addTooltip: true,
+ addLegend: true,
+ enableHover: false,
+ legendPosition: 'right',
+ colorsNumber: 8,
+ colorSchema: 'Blues',
+ setColorRange: false,
+ colorsRange: [],
+ invertColors: false,
+ truncateLegend: true,
+ maxLegendLines: 1,
+ valueAxes: [
+ {
+ id: 'ValueAxis-1',
+ name: 'LeftAxis-1',
+ type: 'value',
+ position: 'left',
+ show: true,
+ style: {},
+ scale: {
+ type: 'linear',
+ mode: 'normal',
+ },
+ labels: {
+ show: true,
+ rotate: 0,
+ filter: false,
+ truncate: 100,
+ overwriteColor: true,
+ },
+ title: {
+ text: 'Count',
+ },
+ },
+ ],
+ },
+ setValue: jest.fn(),
+ } as unknown as HeatmapOptionsProps;
+ });
+
+ it('renders the long legend options for the elastic charts implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapLongLegendsOptions').length).toBe(1);
+ });
+ });
+
+ it('not renders the long legend options for the vislib implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapLongLegendsOptions').length).toBe(0);
+ });
+ });
+
+ it('disables the highlight range switch for the elastic charts implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapHighlightRange').prop('disabled')).toBeTruthy();
+ });
+ });
+
+ it('enables the highlight range switch for the vislib implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapHighlightRange').prop('disabled')).toBeFalsy();
+ });
+ });
+
+ it('disables the color scale dropdown for the elastic charts implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapColorScale').prop('disabled')).toBeTruthy();
+ });
+ });
+
+ it('enables the color scale dropdown for the vislib implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapColorScale').prop('disabled')).toBeFalsy();
+ });
+ });
+
+ it('not renders the scale to data bounds switch for the elastic charts implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapScaleToDataBounds').length).toBe(0);
+ });
+ });
+
+ it('renders the scale to data bounds for the vislib implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapScaleToDataBounds').length).toBe(1);
+ });
+ });
+
+ it('disables the labels rotate for the elastic charts implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapLabelsRotate').prop('disabled')).toBeTruthy();
+ });
+ });
+
+ it('enables the labels rotate for the vislib implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapLabelsRotate').prop('disabled')).toBeFalsy();
+ });
+ });
+
+ it('disables the overwtite color switch for the elastic charts implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(
+ findTestSubject(component, 'heatmapLabelsOverwriteColor').prop('disabled')
+ ).toBeTruthy();
+ });
+ });
+
+ it('enables the overwtite color switch for the vislib implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(
+ findTestSubject(component, 'heatmapLabelsOverwriteColor').prop('disabled')
+ ).toBeFalsy();
+ });
+ });
+
+ it('disables the color picker for the elastic charts implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapLabelsColor').prop('disabled')).toBeTruthy();
+ });
+ });
+
+ it('enables the color picker for the vislib implementation', async () => {
+ component = mountWithIntl( );
+ await act(async () => {
+ expect(findTestSubject(component, 'heatmapLabelsColor').prop('disabled')).toBeFalsy();
+ });
+ });
+});
diff --git a/src/plugins/vis_types/vislib/public/editor/components/heatmap/index.tsx b/src/plugins/vis_types/heatmap/public/editor/components/heatmap.tsx
similarity index 50%
rename from src/plugins/vis_types/vislib/public/editor/components/heatmap/index.tsx
rename to src/plugins/vis_types/heatmap/public/editor/components/heatmap.tsx
index 4e5d43eb7089a..ca5f6d7c44b3c 100644
--- a/src/plugins/vis_types/vislib/public/editor/components/heatmap/index.tsx
+++ b/src/plugins/vis_types/heatmap/public/editor/components/heatmap.tsx
@@ -7,13 +7,10 @@
*/
import React, { useCallback, useEffect, useState } from 'react';
-
-import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPanel, EuiTitle, EuiSpacer, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
-import { ValueAxis } from '../../../../../xy/public';
import {
BasicOptions,
SelectOption,
@@ -24,16 +21,21 @@ import {
ColorSchemaOptions,
NumberInputOption,
PercentageModeOption,
-} from '../../../../../../vis_default_editor/public';
-
-import { HeatmapVisParams } from '../../../heatmap';
+ LongLegendOptions,
+} from '../../../../../vis_default_editor/public';
+import { colorSchemas } from '../../../../../charts/public';
+import { VisEditorOptionsProps } from '../../../../../visualizations/public';
+import { HeatmapVisParams, HeatmapTypeProps, ValueAxis } from '../../types';
import { LabelsPanel } from './labels_panel';
-import { getHeatmapCollections } from './../../collections';
+import { legendPositions, scaleTypes } from '../collections';
-const heatmapCollections = getHeatmapCollections();
+export interface HeatmapOptionsProps
+ extends VisEditorOptionsProps,
+ HeatmapTypeProps {}
-function HeatmapOptions(props: VisEditorOptionsProps) {
- const { stateParams, uiState, setValue, setValidity, setTouched } = props;
+const HeatmapOptions = (props: HeatmapOptionsProps) => {
+ const { stateParams, uiState, setValue, setValidity, setTouched, showElasticChartsOptions } =
+ props;
const [valueAxis] = stateParams.valueAxes;
const isColorsNumberInvalid = stateParams.colorsNumber < 2 || stateParams.colorsNumber > 10;
const [isColorRangesValid, setIsColorRangesValid] = useState(false);
@@ -56,32 +58,55 @@ function HeatmapOptions(props: VisEditorOptionsProps) {
setValidity(stateParams.setColorRange ? isColorRangesValid : !isColorsNumberInvalid);
}, [stateParams.setColorRange, isColorRangesValid, isColorsNumberInvalid, setValidity]);
+ useEffect(() => {
+ if (stateParams.setColorRange) {
+ stateParams.percentageMode = false;
+ }
+ }, [stateParams]);
+
return (
<>
-
+
+ {showElasticChartsOptions && (
+
+ )}
@@ -91,7 +116,7 @@ function HeatmapOptions(props: VisEditorOptionsProps) {
@@ -100,35 +125,54 @@ function HeatmapOptions(props: VisEditorOptionsProps) {
+
+
+
+
-
-
-
+ {!showElasticChartsOptions && (
+
+ )}
@@ -138,7 +182,7 @@ function HeatmapOptions(props: VisEditorOptionsProps) {
data-test-subj="heatmapColorsNumber"
disabled={stateParams.setColorRange}
isInvalid={isColorsNumberInvalid}
- label={i18n.translate('visTypeVislib.controls.heatmapOptions.colorsNumberLabel', {
+ label={i18n.translate('visTypeHeatmap.controls.heatmapOptions.colorsNumberLabel', {
defaultMessage: 'Number of colors',
})}
max={10}
@@ -150,7 +194,7 @@ function HeatmapOptions(props: VisEditorOptionsProps) {
) {
setValue={setValue}
/>
- {stateParams.setColorRange && (
+ {stateParams.setColorRange && stateParams.colorsRange && (
) {
-
+
>
);
-}
+};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
diff --git a/src/plugins/vis_types/heatmap/public/editor/components/index.tsx b/src/plugins/vis_types/heatmap/public/editor/components/index.tsx
new file mode 100644
index 0000000000000..1313d335b06fe
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/editor/components/index.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { lazy } from 'react';
+import { VisEditorOptionsProps } from '../../../../../visualizations/public';
+import { HeatmapVisParams, HeatmapTypeProps } from '../../types';
+
+const HeatmapOptionsLazy = lazy(() => import('./heatmap'));
+
+export const getHeatmapOptions =
+ ({ showElasticChartsOptions, palettes, trackUiMetric }: HeatmapTypeProps) =>
+ (props: VisEditorOptionsProps) =>
+ (
+
+ );
diff --git a/src/plugins/vis_types/vislib/public/editor/components/heatmap/labels_panel.tsx b/src/plugins/vis_types/heatmap/public/editor/components/labels_panel.tsx
similarity index 62%
rename from src/plugins/vis_types/vislib/public/editor/components/heatmap/labels_panel.tsx
rename to src/plugins/vis_types/heatmap/public/editor/components/labels_panel.tsx
index e5e41500e39e7..3bdfcb34eb13b 100644
--- a/src/plugins/vis_types/vislib/public/editor/components/heatmap/labels_panel.tsx
+++ b/src/plugins/vis_types/heatmap/public/editor/components/labels_panel.tsx
@@ -13,19 +13,18 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
-import { SwitchOption } from '../../../../../../vis_default_editor/public';
-import { ValueAxis } from '../../../../../xy/public';
-
-import { HeatmapVisParams } from '../../../heatmap';
+import { SwitchOption } from '../../../../../vis_default_editor/public';
+import { HeatmapVisParams, ValueAxis } from '../../types';
const VERTICAL_ROTATION = 270;
interface LabelsPanelProps {
valueAxis: ValueAxis;
setValue: VisEditorOptionsProps['setValue'];
+ isNewLibrary?: boolean;
}
-function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) {
+function LabelsPanel({ valueAxis, setValue, isNewLibrary }: LabelsPanelProps) {
const rotateLabels = valueAxis.labels.rotate === VERTICAL_ROTATION;
const setValueAxisLabels = useCallback(
@@ -55,7 +54,7 @@ function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) {
@@ -63,48 +62,59 @@ function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) {
diff --git a/src/plugins/vis_types/heatmap/public/index.ts b/src/plugins/vis_types/heatmap/public/index.ts
new file mode 100644
index 0000000000000..34387430adbe0
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { VisTypeHeatmapPlugin } from './plugin';
+
+export { heatmapVisType } from './vis_type';
+
+export const plugin = () => new VisTypeHeatmapPlugin();
diff --git a/src/plugins/vis_types/heatmap/public/plugin.ts b/src/plugins/vis_types/heatmap/public/plugin.ts
new file mode 100644
index 0000000000000..622f68ed707e5
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/plugin.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { CoreSetup } from 'src/core/public';
+import type { VisualizationsSetup } from '../../../visualizations/public';
+import type { ChartsPluginSetup } from '../../../charts/public';
+import type { FieldFormatsStart } from '../../../field_formats/public';
+import type { UsageCollectionSetup } from '../../../usage_collection/public';
+import type { DataPublicPluginStart } from '../../../data/public';
+import { LEGACY_HEATMAP_CHARTS_LIBRARY } from '../common';
+import { heatmapVisType } from './vis_type';
+
+/** @internal */
+export interface VisTypeHeatmapSetupDependencies {
+ visualizations: VisualizationsSetup;
+ charts: ChartsPluginSetup;
+ usageCollection: UsageCollectionSetup;
+}
+
+/** @internal */
+export interface VisTypeHeatmapPluginStartDependencies {
+ data: DataPublicPluginStart;
+ fieldFormats: FieldFormatsStart;
+}
+
+export class VisTypeHeatmapPlugin {
+ setup(
+ core: CoreSetup,
+ { visualizations, charts, usageCollection }: VisTypeHeatmapSetupDependencies
+ ) {
+ if (!core.uiSettings.get(LEGACY_HEATMAP_CHARTS_LIBRARY)) {
+ const trackUiMetric = usageCollection?.reportUiCounter.bind(
+ usageCollection,
+ 'vis_type_heatmap'
+ );
+
+ visualizations.createBaseVisualization(
+ heatmapVisType({
+ showElasticChartsOptions: true,
+ palettes: charts.palettes,
+ trackUiMetric,
+ })
+ );
+ }
+ return {};
+ }
+
+ start() {}
+}
diff --git a/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts
new file mode 100644
index 0000000000000..a7e9f53e703ec
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts
@@ -0,0 +1,1792 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+export const sampleAreaVis = {
+ type: {
+ name: 'heatmap',
+ title: 'Heatmap',
+ description: 'Creates a heatmap viz',
+ icon: 'visHeatmap',
+ stage: 'production',
+ options: {
+ showTimePicker: true,
+ showQueryBar: true,
+ showFilterBar: true,
+ showIndexSelection: true,
+ hierarchicalData: false,
+ },
+ visConfig: {
+ defaults: {
+ type: 'heatmap',
+ valueAxes: [
+ {
+ id: 'ValueAxis-1',
+ name: 'LeftAxis-1',
+ type: 'value',
+ position: 'left',
+ show: true,
+ style: {},
+ scale: {
+ type: 'linear',
+ mode: 'normal',
+ },
+ labels: {
+ show: true,
+ rotate: 0,
+ filter: false,
+ truncate: 100,
+ },
+ title: {
+ text: 'Count',
+ },
+ },
+ ],
+ percentageMode: false,
+ addTooltip: true,
+ addLegend: true,
+ enableHover: false,
+ legendPosition: 'right',
+ colorsNumber: 8,
+ colorSchema: 'Blues',
+ setColorRange: false,
+ colorsRange: [],
+ invertColors: false,
+ truncateLegend: true,
+ maxLegendLines: 1,
+ },
+ },
+ editorConfig: {
+ optionTabs: [
+ {
+ name: 'advanced',
+ title: 'Metrics & axes',
+ },
+ {
+ name: 'options',
+ title: 'Panel settings',
+ },
+ ],
+ schemas: {
+ all: [
+ {
+ group: 'metrics',
+ name: 'metric',
+ title: 'Y-axis',
+ aggFilter: ['!geo_centroid', '!geo_bounds'],
+ min: 1,
+ defaults: [
+ {
+ schema: 'metric',
+ type: 'count',
+ },
+ ],
+ max: null,
+ editor: false,
+ params: [],
+ },
+ {
+ group: 'metrics',
+ name: 'radius',
+ title: 'Dot size',
+ min: 0,
+ max: 1,
+ aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'],
+ editor: false,
+ params: [],
+ },
+ {
+ group: 'buckets',
+ name: 'segment',
+ title: 'X-axis',
+ min: 0,
+ max: 1,
+ aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
+ editor: false,
+ params: [],
+ },
+ {
+ group: 'buckets',
+ name: 'group',
+ title: 'Split series',
+ min: 0,
+ max: 3,
+ aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
+ editor: false,
+ params: [],
+ },
+ {
+ group: 'buckets',
+ name: 'split',
+ title: 'Split chart',
+ min: 0,
+ max: 1,
+ aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
+ params: [
+ {
+ name: 'row',
+ default: true,
+ },
+ ],
+ editor: false,
+ },
+ ],
+ buckets: [null, null, null],
+ metrics: [null, null],
+ },
+ },
+ hidden: false,
+ hierarchicalData: false,
+ },
+ title: '[eCommerce] Sales by Category',
+ description: '',
+ params: {
+ type: 'heatmap',
+ valueAxes: [
+ {
+ id: 'ValueAxis-1',
+ name: 'LeftAxis-1',
+ type: 'value',
+ position: 'left',
+ show: true,
+ style: {},
+ scale: {
+ type: 'linear',
+ mode: 'normal',
+ },
+ labels: {
+ show: true,
+ rotate: 0,
+ filter: false,
+ truncate: 100,
+ },
+ title: {
+ text: 'Sum of total_quantity',
+ },
+ },
+ ],
+ percentageMode: false,
+ addTooltip: true,
+ addLegend: true,
+ enableHover: false,
+ legendPosition: 'right',
+ colorsNumber: 8,
+ colorSchema: 'Blues',
+ setColorRange: false,
+ colorsRange: [],
+ invertColors: false,
+ truncateLegend: true,
+ maxLegendLines: 1,
+ dimensions: {
+ x: {
+ accessor: 0,
+ format: {
+ id: 'date',
+ params: {
+ pattern: 'YYYY-MM-DD HH:mm',
+ },
+ },
+ params: {
+ date: true,
+ interval: 43200000,
+ format: 'YYYY-MM-DD HH:mm',
+ bounds: {
+ min: '2020-09-30T12:41:13.795Z',
+ max: '2020-10-15T17:00:00.000Z',
+ },
+ },
+ label: 'order_date per 12 hours',
+ aggType: 'date_histogram',
+ },
+ y: [
+ {
+ accessor: 2,
+ format: {
+ id: 'number',
+ params: {
+ parsedUrl: {
+ origin: 'http://localhost:5801',
+ pathname: '/app/visualize',
+ basePath: '',
+ },
+ },
+ },
+ params: {},
+ label: 'Sum of total_quantity',
+ aggType: 'sum',
+ },
+ ],
+ series: [
+ {
+ accessor: 1,
+ format: {
+ id: 'terms',
+ params: {
+ id: 'string',
+ otherBucketLabel: 'Other',
+ missingBucketLabel: 'Missing',
+ },
+ },
+ params: {},
+ label: 'category.keyword: Descending',
+ aggType: 'terms',
+ },
+ ],
+ },
+ },
+ data: {
+ searchSource: {
+ id: 'data_source1',
+ requestStartHandlers: [],
+ inheritOptions: {},
+ history: [],
+ fields: {
+ query: {
+ query: '',
+ language: 'kuery',
+ },
+ filter: [],
+ index: {
+ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ title: 'kibana_sample_data_ecommerce',
+ fieldFormatMap: {
+ taxful_total_price: {
+ id: 'number',
+ params: {
+ pattern: '$0,0.[00]',
+ },
+ },
+ },
+ fields: [
+ {
+ count: 0,
+ name: '_id',
+ type: 'string',
+ esTypes: ['_id'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: '_index',
+ type: 'string',
+ esTypes: ['_index'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: '_score',
+ type: 'number',
+ scripted: false,
+ searchable: false,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: '_source',
+ type: '_source',
+ esTypes: ['_source'],
+ scripted: false,
+ searchable: false,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: '_type',
+ type: 'string',
+ esTypes: ['_type'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'category',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'category.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'category',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'currency',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'customer_birth_date',
+ type: 'date',
+ esTypes: ['date'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'customer_first_name',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'customer_first_name.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'customer_first_name',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'customer_full_name',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'customer_full_name.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'customer_full_name',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'customer_gender',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'customer_id',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'customer_last_name',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'customer_last_name.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'customer_last_name',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'customer_phone',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'day_of_week',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'day_of_week_i',
+ type: 'number',
+ esTypes: ['integer'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'email',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'event.dataset',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'geoip.city_name',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'geoip.continent_name',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'geoip.country_iso_code',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'geoip.location',
+ type: 'geo_point',
+ esTypes: ['geo_point'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'geoip.region_name',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'manufacturer',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'manufacturer.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'manufacturer',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'order_date',
+ type: 'date',
+ esTypes: ['date'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'order_id',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products._id',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'products._id.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'products._id',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'products.base_price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.base_unit_price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.category',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'products.category.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'products.category',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'products.created_on',
+ type: 'date',
+ esTypes: ['date'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.discount_amount',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.discount_percentage',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.manufacturer',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'products.manufacturer.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'products.manufacturer',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'products.min_price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.product_id',
+ type: 'number',
+ esTypes: ['long'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.product_name',
+ type: 'string',
+ esTypes: ['text'],
+ scripted: false,
+ searchable: true,
+ aggregatable: false,
+ readFromDocValues: false,
+ },
+ {
+ count: 0,
+ name: 'products.product_name.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ subType: {
+ multi: {
+ parent: 'products.product_name',
+ },
+ },
+ },
+ {
+ count: 0,
+ name: 'products.quantity',
+ type: 'number',
+ esTypes: ['integer'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.sku',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.tax_amount',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.taxful_price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.taxless_price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'products.unit_discount_amount',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'sku',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'taxful_total_price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'taxless_total_price',
+ type: 'number',
+ esTypes: ['half_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'total_quantity',
+ type: 'number',
+ esTypes: ['integer'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'total_unique_products',
+ type: 'number',
+ esTypes: ['integer'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'type',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'user',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ ],
+ timeFieldName: 'order_date',
+ metaFields: ['_source', '_id', '_type', '_index', '_score'],
+ version: 'WzEzLDFd',
+ originalSavedObjectBody: {
+ title: 'kibana_sample_data_ecommerce',
+ timeFieldName: 'order_date',
+ fields:
+ '[{"count":0,"name":"_id","type":"string","esTypes":["_id"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_index","type":"string","esTypes":["_index"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_score","type":"number","scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_source","type":"_source","esTypes":["_source"],"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_type","type":"string","esTypes":["_type"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"category","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"category.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"category"}}},{"count":0,"name":"currency","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_birth_date","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_first_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_first_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_first_name"}}},{"count":0,"name":"customer_full_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_full_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_full_name"}}},{"count":0,"name":"customer_gender","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_id","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_last_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_last_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_last_name"}}},{"count":0,"name":"customer_phone","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"day_of_week","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"day_of_week_i","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"email","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"event.dataset","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.city_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.continent_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.country_iso_code","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.location","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.region_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"manufacturer","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"manufacturer.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"manufacturer"}}},{"count":0,"name":"order_date","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"order_id","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products._id","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products._id.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products._id"}}},{"count":0,"name":"products.base_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.base_unit_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.category","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.category.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.category"}}},{"count":0,"name":"products.created_on","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.discount_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.discount_percentage","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.manufacturer","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.manufacturer.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.manufacturer"}}},{"count":0,"name":"products.min_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.product_id","type":"number","esTypes":["long"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.product_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.product_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.product_name"}}},{"count":0,"name":"products.quantity","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.sku","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.tax_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.taxful_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.taxless_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.unit_discount_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"sku","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"taxful_total_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"taxless_total_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"total_quantity","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"total_unique_products","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"type","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"user","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]',
+ fieldFormatMap:
+ '{"taxful_total_price":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5801","pathname":"/app/visualize","basePath":""},"pattern":"$0,0.[00]"}}}',
+ },
+ shortDotsEnable: false,
+ fieldFormats: {
+ fieldFormats: {},
+ defaultMap: {
+ ip: {
+ id: 'ip',
+ params: {},
+ },
+ date: {
+ id: 'date',
+ params: {},
+ },
+ date_nanos: {
+ id: 'date_nanos',
+ params: {},
+ es: true,
+ },
+ number: {
+ id: 'number',
+ params: {},
+ },
+ boolean: {
+ id: 'boolean',
+ params: {},
+ },
+ _source: {
+ id: '_source',
+ params: {},
+ },
+ _default_: {
+ id: 'string',
+ params: {},
+ },
+ },
+ metaParamsOptions: {},
+ },
+ },
+ },
+ dependencies: {
+ legacy: {
+ loadingCount$: {
+ _isScalar: false,
+ observers: [
+ {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ destination: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ destination: {
+ closed: true,
+ },
+ _context: {},
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ count: 1,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ hasPrev: true,
+ prev: 0,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [],
+ active: 1,
+ index: 2,
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [
+ {
+ _isScalar: false,
+ },
+ ],
+ active: 1,
+ index: 1,
+ },
+ },
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ subject: {
+ _isScalar: false,
+ observers: [
+ {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ destination: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ _context: {},
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ count: 13,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ hasPrev: true,
+ prev: 0,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [],
+ active: 1,
+ index: 2,
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [
+ {
+ _isScalar: false,
+ },
+ ],
+ active: 1,
+ index: 1,
+ },
+ },
+ _subscriptions: [
+ null,
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ subject: {
+ _isScalar: false,
+ observers: [null],
+ closed: false,
+ isStopped: false,
+ hasError: false,
+ thrownError: null,
+ _value: 0,
+ },
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ hasKey: true,
+ key: 0,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ seenValue: false,
+ },
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ destination: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ _context: {},
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ count: 1,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ hasPrev: true,
+ prev: 0,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [],
+ active: 1,
+ index: 2,
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [
+ {
+ _isScalar: false,
+ },
+ ],
+ active: 1,
+ index: 1,
+ },
+ },
+ _subscriptions: [
+ null,
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ subject: {
+ _isScalar: false,
+ observers: [null],
+ closed: false,
+ isStopped: false,
+ hasError: false,
+ thrownError: null,
+ _value: 0,
+ },
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ hasKey: true,
+ key: 0,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ seenValue: false,
+ },
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ destination: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ _context: {},
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ count: 1,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ hasPrev: true,
+ prev: 0,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [],
+ active: 1,
+ index: 2,
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [
+ {
+ _isScalar: false,
+ },
+ ],
+ active: 1,
+ index: 1,
+ },
+ },
+ _subscriptions: [
+ null,
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ subject: {
+ _isScalar: false,
+ observers: [null],
+ closed: false,
+ isStopped: false,
+ hasError: false,
+ thrownError: null,
+ _value: 0,
+ },
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ hasKey: true,
+ key: 0,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ seenValue: false,
+ },
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ destination: {
+ closed: false,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ _context: {},
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ count: 3,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: false,
+ hasPrev: true,
+ prev: 0,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: true,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [],
+ active: 1,
+ index: 2,
+ },
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ parent: {
+ closed: true,
+ _parentOrParents: null,
+ _subscriptions: null,
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: true,
+ concurrent: 1,
+ hasCompleted: true,
+ buffer: [
+ {
+ _isScalar: false,
+ },
+ ],
+ active: 1,
+ index: 1,
+ },
+ },
+ _subscriptions: [
+ null,
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ subject: {
+ _isScalar: false,
+ observers: [null],
+ closed: false,
+ isStopped: false,
+ hasError: false,
+ thrownError: null,
+ _value: 0,
+ },
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ hasKey: true,
+ key: 0,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ seenValue: false,
+ },
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ null,
+ ],
+ closed: false,
+ isStopped: false,
+ hasError: false,
+ thrownError: null,
+ },
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ null,
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ seenValue: false,
+ },
+ _subscriptions: [null],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ },
+ _subscriptions: [
+ {
+ closed: false,
+ _subscriptions: null,
+ },
+ ],
+ syncErrorValue: null,
+ syncErrorThrown: false,
+ syncErrorThrowable: false,
+ isStopped: false,
+ hasKey: true,
+ key: 0,
+ },
+ ],
+ closed: false,
+ isStopped: false,
+ hasError: false,
+ thrownError: null,
+ _value: 0,
+ },
+ },
+ },
+ },
+ aggs: {
+ typesRegistry: {},
+ bySchemaName: () => [
+ {
+ id: '1',
+ enabled: true,
+ type: 'sum',
+ params: {
+ field: 'total_quantity',
+ },
+ schema: 'metric',
+ makeLabel: () => 'Total quantity',
+ toSerializedFieldFormat: () => ({
+ id: 'number',
+ params: {
+ parsedUrl: {
+ origin: 'http://localhost:5801',
+ pathname: '/app/visualize',
+ basePath: '',
+ },
+ },
+ }),
+ },
+ ],
+ getResponseAggs: () => [
+ {
+ id: '1',
+ enabled: true,
+ type: 'sum',
+ params: {
+ field: 'total_quantity',
+ },
+ schema: 'metric',
+ toSerializedFieldFormat: () => ({
+ id: 'number',
+ params: {
+ parsedUrl: {
+ origin: 'http://localhost:5801',
+ pathname: '/app/visualize',
+ basePath: '',
+ },
+ },
+ }),
+ },
+ {
+ id: '2',
+ enabled: true,
+ type: 'date_histogram',
+ params: {
+ field: 'order_date',
+ timeRange: {
+ from: '2020-09-30T12:41:13.795Z',
+ to: '2020-10-15T17:00:00.000Z',
+ },
+ useNormalizedEsInterval: true,
+ scaleMetricValues: false,
+ interval: 'auto',
+ drop_partials: false,
+ min_doc_count: 1,
+ extended_bounds: {},
+ },
+ schema: 'segment',
+ toSerializedFieldFormat: () => ({
+ id: 'date',
+ params: { pattern: 'HH:mm:ss.SSS' },
+ }),
+ },
+ {
+ id: '3',
+ enabled: true,
+ type: 'terms',
+ params: {
+ field: 'category.keyword',
+ orderBy: '1',
+ order: 'desc',
+ size: 5,
+ otherBucket: false,
+ otherBucketLabel: 'Other',
+ missingBucket: false,
+ missingBucketLabel: 'Missing',
+ },
+ schema: 'group',
+ toSerializedFieldFormat: () => ({
+ id: 'terms',
+ params: {
+ id: 'string',
+ otherBucketLabel: 'Other',
+ missingBucketLabel: 'Missing',
+ parsedUrl: {
+ origin: 'http://localhost:5801',
+ pathname: '/app/visualize',
+ basePath: '',
+ },
+ },
+ }),
+ },
+ ],
+ },
+ },
+ isHierarchical: () => false,
+ uiState: {},
+};
diff --git a/src/plugins/vis_types/heatmap/public/to_ast.test.ts b/src/plugins/vis_types/heatmap/public/to_ast.test.ts
new file mode 100644
index 0000000000000..ec6dfd92e5d6a
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/to_ast.test.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Vis } from 'src/plugins/visualizations/public';
+import { sampleAreaVis } from './sample_vis.test.mocks';
+import { buildExpression } from '../../../expressions/public';
+
+import { toExpressionAst } from './to_ast';
+import { HeatmapVisParams } from './types';
+
+jest.mock('../../../expressions/public', () => ({
+ ...(jest.requireActual('../../../expressions/public') as any),
+ buildExpression: jest.fn().mockImplementation(() => ({
+ toAst: () => ({
+ type: 'expression',
+ chain: [],
+ }),
+ })),
+}));
+
+jest.mock('./to_ast_esaggs', () => ({
+ getEsaggsFn: jest.fn(),
+}));
+
+describe('heatmap vis toExpressionAst function', () => {
+ let vis: Vis;
+
+ const params = {
+ timefilter: {},
+ timeRange: {},
+ abortSignal: {},
+ } as any;
+
+ beforeEach(() => {
+ vis = sampleAreaVis as any;
+ });
+
+ it('should match basic snapshot', () => {
+ toExpressionAst(vis, params);
+ const [, builtExpression] = (buildExpression as jest.Mock).mock.calls.pop()[0];
+
+ expect(builtExpression).toMatchSnapshot();
+ });
+});
diff --git a/src/plugins/vis_types/heatmap/public/to_ast.ts b/src/plugins/vis_types/heatmap/public/to_ast.ts
new file mode 100644
index 0000000000000..d4fa5c8574dfe
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/to_ast.ts
@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { VisToExpressionAst, getVisSchemas, SchemaConfig } from '../../../visualizations/public';
+import { buildExpression, buildExpressionFunction } from '../../../expressions/public';
+import { getStopsWithColorsFromRanges, getStopsWithColorsFromColorsNumber } from './utils/palette';
+import type { HeatmapVisParams } from './types';
+import { getEsaggsFn } from './to_ast_esaggs';
+
+const DEFAULT_PERCENT_DECIMALS = 2;
+
+const prepareLegend = (params: HeatmapVisParams) => {
+ const legend = buildExpressionFunction('heatmap_legend', {
+ isVisible: params.addLegend,
+ position: params.legendPosition,
+ shouldTruncate: params.truncateLegend ?? true,
+ maxLines: params.maxLegendLines ?? 1,
+ });
+
+ return buildExpression([legend]);
+};
+
+const prepareDimension = (params: SchemaConfig) => {
+ const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor });
+
+ if (params.format) {
+ visdimension.addArgument('format', params.format.id);
+ visdimension.addArgument('formatParams', JSON.stringify(params.format.params));
+ }
+
+ return buildExpression([visdimension]);
+};
+
+const prepareGrid = (params: HeatmapVisParams) => {
+ const gridConfig = buildExpressionFunction('heatmap_grid', {
+ isCellLabelVisible: params.valueAxes?.[0].labels.show ?? false,
+ isXAxisLabelVisible: true,
+ });
+
+ return buildExpression([gridConfig]);
+};
+
+export const toExpressionAst: VisToExpressionAst = async (vis, params) => {
+ const schemas = getVisSchemas(vis, params);
+
+ // fix formatter for percentage mode
+ if (vis.params.percentageMode === true) {
+ schemas.metric.forEach((metric: SchemaConfig) => {
+ metric.format = {
+ id: 'percent',
+ params: {
+ pattern:
+ vis.params.percentageFormatPattern ?? `0,0.[${'0'.repeat(DEFAULT_PERCENT_DECIMALS)}]%`,
+ },
+ };
+ });
+ }
+
+ const expressionArgs = {
+ showTooltip: vis.params.addTooltip,
+ highlightInHover: vis.params.enableHover,
+ lastRangeIsRightOpen: vis.params.lastRangeIsRightOpen ?? false,
+ percentageMode: vis.params.percentageMode,
+ legend: prepareLegend(vis.params),
+ gridConfig: prepareGrid(vis.params),
+ };
+
+ const visTypeHeatmap = buildExpressionFunction('heatmap', expressionArgs);
+ if (schemas.metric.length) {
+ visTypeHeatmap.addArgument('valueAccessor', prepareDimension(schemas.metric[0]));
+ }
+ if (schemas.segment && schemas.segment.length) {
+ visTypeHeatmap.addArgument('xAccessor', prepareDimension(schemas.segment[0]));
+ }
+ if (schemas.group && schemas.group.length) {
+ visTypeHeatmap.addArgument('yAccessor', prepareDimension(schemas.group[0]));
+ }
+ if (schemas.split_row && schemas.split_row.length) {
+ visTypeHeatmap.addArgument('splitRowAccessor', prepareDimension(schemas.split_row[0]));
+ }
+ if (schemas.split_column && schemas.split_column.length) {
+ visTypeHeatmap.addArgument('splitColumnAccessor', prepareDimension(schemas.split_column[0]));
+ }
+ let palette;
+ if (vis.params.setColorRange && vis.params.colorsRange && vis.params.colorsRange.length) {
+ const stopsWithColors = getStopsWithColorsFromRanges(
+ vis.params.colorsRange,
+ vis.params.colorSchema,
+ vis.params.invertColors
+ );
+ // palette is type of number, if user gives specific ranges
+ palette = buildExpressionFunction('palette', {
+ ...stopsWithColors,
+ range: 'number',
+ continuity: 'none',
+ rangeMin:
+ vis.params.setColorRange && vis.params.colorsRange && vis.params.colorsRange.length
+ ? vis.params.colorsRange[0].from
+ : undefined,
+ rangeMax:
+ vis.params.setColorRange && vis.params.colorsRange && vis.params.colorsRange.length
+ ? vis.params.colorsRange[vis.params?.colorsRange.length - 1].to
+ : undefined,
+ });
+ } else {
+ // palette is type of percent, if user wants dynamic calulated ranges
+ const stopsWithColors = getStopsWithColorsFromColorsNumber(
+ vis.params.colorsNumber,
+ vis.params.colorSchema,
+ vis.params.invertColors
+ );
+ palette = buildExpressionFunction('palette', {
+ ...stopsWithColors,
+ range: 'percent',
+ continuity: 'none',
+ rangeMin: 0,
+ rangeMax: 100,
+ });
+ }
+ visTypeHeatmap.addArgument('palette', buildExpression([palette]));
+
+ const ast = buildExpression([getEsaggsFn(vis), visTypeHeatmap]);
+
+ return ast.toAst();
+};
diff --git a/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts b/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts
new file mode 100644
index 0000000000000..9b6e02928f7a9
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Vis } from '../../../visualizations/public';
+import { buildExpression, buildExpressionFunction } from '../../../expressions/public';
+import {
+ EsaggsExpressionFunctionDefinition,
+ IndexPatternLoadExpressionFunctionDefinition,
+} from '../../../data/public';
+
+import { HeatmapVisParams } from './types';
+
+/**
+ * Get esaggs expressions function
+ * @param vis
+ */
+export function getEsaggsFn(vis: Vis) {
+ return buildExpressionFunction('esaggs', {
+ index: buildExpression([
+ buildExpressionFunction('indexPatternLoad', {
+ id: vis.data.indexPattern!.id!,
+ }),
+ ]),
+ metricsAtAllLevels: vis.isHierarchical(),
+ partialRows: false,
+ aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())),
+ });
+}
diff --git a/src/plugins/vis_types/heatmap/public/types.ts b/src/plugins/vis_types/heatmap/public/types.ts
new file mode 100644
index 0000000000000..b02dad8656c83
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/types.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { UiCounterMetricType } from '@kbn/analytics';
+import type { Position } from '@elastic/charts';
+import type { ChartsPluginSetup, Style, Labels, ColorSchemas } from '../../../charts/public';
+import { Range } from '../../../expressions/public';
+
+export interface HeatmapTypeProps {
+ showElasticChartsOptions?: boolean;
+ palettes?: ChartsPluginSetup['palettes'];
+ trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
+}
+
+export interface HeatmapVisParams {
+ addLegend: boolean;
+ addTooltip: boolean;
+ enableHover: boolean;
+ legendPosition: Position;
+ truncateLegend?: boolean;
+ maxLegendLines?: number;
+ lastRangeIsRightOpen: boolean;
+ percentageMode: boolean;
+ valueAxes: ValueAxis[];
+ colorSchema: ColorSchemas;
+ invertColors: boolean;
+ colorsNumber: number | '';
+ setColorRange: boolean;
+ colorsRange?: Range[];
+ percentageFormatPattern?: string;
+}
+
+// ToDo: move them to constants
+export enum ScaleType {
+ Linear = 'linear',
+ Log = 'log',
+ SquareRoot = 'square root',
+}
+
+export enum AxisType {
+ Category = 'category',
+ Value = 'value',
+}
+export enum AxisMode {
+ Normal = 'normal',
+ Percentage = 'percentage',
+ Wiggle = 'wiggle',
+ Silhouette = 'silhouette',
+}
+
+export interface Scale {
+ boundsMargin?: number | '';
+ defaultYExtents?: boolean;
+ max?: number | null;
+ min?: number | null;
+ mode?: AxisMode;
+ setYExtents?: boolean;
+ type: ScaleType;
+}
+
+interface CategoryAxis {
+ id: string;
+ labels: Labels;
+ position: Position;
+ scale: Scale;
+ show: boolean;
+ title?: {
+ text?: string;
+ };
+ type: AxisType;
+ style?: Partial