diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index efba93350b8fb..2a5fc914662b6 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -11,7 +11,7 @@ Delete any items that are not applicable to this PR.
- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
-- [ ] If a plugin configuration key changed, check if it needs to be whitelisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker)
+- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md
new file mode 100644
index 0000000000000..0fdf36bc719ec
--- /dev/null
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md)
+
+## ExpressionsInspectorAdapter.ast property
+
+Signature:
+
+```typescript
+get ast(): any;
+```
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md
new file mode 100644
index 0000000000000..671270a5c78ce
--- /dev/null
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [logAST](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md)
+
+## ExpressionsInspectorAdapter.logAST() method
+
+Signature:
+
+```typescript
+logAST(ast: any): void;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| ast | any | |
+
+Returns:
+
+`void`
+
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md
new file mode 100644
index 0000000000000..23d542a0f69eb
--- /dev/null
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md)
+
+## ExpressionsInspectorAdapter class
+
+Signature:
+
+```typescript
+export declare class ExpressionsInspectorAdapter extends EventEmitter
+```
+
+## Properties
+
+| Property | Modifiers | Type | Description |
+| --- | --- | --- | --- |
+| [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) | | any | |
+
+## Methods
+
+| Method | Modifiers | Description |
+| --- | --- | --- |
+| [logAST(ast)](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) | | |
+
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md
index 1b97c9e11f83c..e3eb7a34175ee 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md
@@ -16,6 +16,7 @@
| [ExpressionRenderer](./kibana-plugin-plugins-expressions-public.expressionrenderer.md) | |
| [ExpressionRendererRegistry](./kibana-plugin-plugins-expressions-public.expressionrendererregistry.md) | |
| [ExpressionRenderHandler](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.md) | |
+| [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) | |
| [ExpressionsPublicPlugin](./kibana-plugin-plugins-expressions-public.expressionspublicplugin.md) | |
| [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) | ExpressionsService class is used for multiple purposes:1. It implements the same Expressions service that can be used on both: (1) server-side and (2) browser-side. 2. It implements the same Expressions service that users can fork/clone, thus have their own instance of the Expressions plugin. 3. ExpressionsService defines the public contracts of \*setup\* and \*start\* Kibana Platform life-cycles for ease-of-use on server-side and browser-side. 4. ExpressionsService creates a bound version of all exported contract functions. 5. Functions are bound the way there are:\`\`\`ts registerFunction = (...args: Parameters<Executor\['registerFunction'\]> ): ReturnType<Executor\['registerFunction'\]> => this.executor.registerFunction(...args); \`\`\`so that JSDoc appears in developers IDE when they use those plugins.expressions.registerFunction(. |
| [ExpressionType](./kibana-plugin-plugins-expressions-public.expressiontype.md) | |
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 7e7c8953fd527..c2306b80734d8 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -458,7 +458,7 @@ of buckets to try to represent.
[horizontal]
[[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`::
-Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation.
+Enables legacy charts library for area, line and bar charts in visualize.
[[visualization-colormapping]]`visualization:colorMapping`::
**This setting is deprecated and will not be supported as of 8.0.**
diff --git a/docs/management/managing-tags.asciidoc b/docs/management/managing-tags.asciidoc
index 3da98b2281fdc..88fdef66a7418 100644
--- a/docs/management/managing-tags.asciidoc
+++ b/docs/management/managing-tags.asciidoc
@@ -37,8 +37,7 @@ Create a tag to assign to your saved objects.
image::images/tags/create-tag.png[Tag creation popin]
. Enter a name and select a color for the new tag.
+
-The name can include alphanumeric characters (English letters and digits), `:`, `-`, `_` and the space character,
-and cannot be longer than 50 characters.
+The name cannot be longer than 50 characters.
. Click *Create tag*.
[float]
diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc
index 74d097164c4a7..7436536d22781 100644
--- a/docs/setup/upgrade/upgrade-migrations.asciidoc
+++ b/docs/setup/upgrade/upgrade-migrations.asciidoc
@@ -1,11 +1,13 @@
[[upgrade-migrations]]
=== Upgrade migrations
-Every time {kib} is upgraded it checks to see if all saved objects, such as dashboards, visualizations, and index patterns, are compatible with the new version. If any saved objects need to be updated, then the automatic saved object migration process is kicked off.
+Every time {kib} is upgraded it will perform an upgrade migration to ensure that all <> are compatible with the new version.
NOTE: 6.7 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[Upgrade Assistant]
to help you prepare for your upgrade to 7.0. To access the assistant, go to *Management > 7.0 Upgrade Assistant*.
+WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Be sure to read the documentation for your version of {kib} before proceeding.
+
WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings were changed these instructions will have to be adapted accordingly.
[float]
@@ -14,19 +16,35 @@ WARNING: The following instructions assumes {kib} is using the default index nam
Saved objects are stored in two indices:
-* `.kibana_N`, or if set, the `kibana.index` configuration setting
-* `.kibana_task_manager_N`, or if set, the `xpack.tasks.index` configuration setting
+* `.kibana_{kibana_version}_001`, or if the `kibana.index` configuration setting is set `.{kibana.index}_{kibana_version}_001`. E.g. for Kibana v7.12.0 `.kibana_7.12.0_001`.
+* `.kibana_task_manager_{kibana_version}_001`, or if the `xpack.tasks.index` configuration setting is set `.{xpack.tasks.index}_{kibana_version}_001` E.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`.
-For each of these indices, `N` is a number that increments every time {kib} runs an upgrade migration on that index. The index aliases `.kibana` and `.kibana_task_manager` point to the most up-to-date index.
+The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date version indices.
+
+The first time a newer {kib} starts, it will first perform an upgrade migration before starting plugins or serving HTTP traffic. To prevent losing acknowledged writes old nodes should be shutdown before starting the upgrade. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later will add a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}.
+
+.Saved object indices and aliases per {kib} version
+[options="header"]
+[cols="a,a,a"]
+|=======================
+|Upgrading from version | Outdated index (alias) | Upgraded index (alias)
+| 6.0.0 through 6.4.x | `.kibana` 1.3+^.^| `.kibana_7.12.0_001`
+(`.kibana` alias)
+
+`.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias)
+| 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias)
+| 7.4.0 through 7.11.x
+| `.kibana_N` (`.kibana` alias)
-While {kib} is starting up and before serving any HTTP traffic, it checks to see if any internal mapping changes or data transformations for existing saved objects are required.
+`.kibana_task_manager_N` (`.kibana_task_manager` alias)
+|=======================
-When changes are necessary, a new migration is started. To ensure that only one {kib} instance performs the migration, each instance will attempt to obtain a migration lock by creating a new `.kibana_N+1` index. The instance that succeeds in creating the index will then read batches of documents from the existing index, migrate them, and write them to the new index. Once the objects are migrated, the lock is released by pointing the `.kibana` index alias the new upgraded `.kibana_N+1` index.
+==== Upgrading multiple {kib} instances
+When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade.
-Instances that failed to acquire a lock will log `Another Kibana instance appears to be migrating the index. Waiting for that migration to complete`. The instance will then wait until `.kibana` points to an upgraded index before starting up and serving HTTP traffic.
+Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel.
-NOTE: Prior to 6.5.0, saved objects were stored directly in an index named `.kibana`. After upgrading to version 6.5+, {kib} will migrate this index into `.kibana_N` and set `.kibana` up as an index alias. +
-Prior to 7.4.0, task manager tasks were stored directly in an index name `.kibana_task_manager`. After upgrading to version 7.4+, {kib} will migrate this index into `.kibana_task_manager_N` and set `.kibana_task_manager` up as an index alias.
+For large deployments with more than 10 {kib} instances and more than 10 000 saved objects, the upgrade downtime can be reduced by bringing up a single {kib} instance and waiting for it to complete the upgrade migration before bringing up the remaining instances.
[float]
[[preventing-migration-failures]]
@@ -54,50 +72,31 @@ Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensu
* a "green" cluster status
[float]
-===== Running different versions of {kib} connected to the same {es} index
-Kibana does not support rolling upgrades. Stop all {kib} instances before starting a newer version to prevent upgrade failures and data loss.
+===== Different versions of {kib} connected to the same {es} index
+When different versions of {kib} are attempting an upgrade migration in parallel this can lead to migration failures. Ensure that all {kib} instances are running the same version, configuration and plugins.
[float]
===== Incompatible `xpack.tasks.index` configuration setting
-For {kib} < 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting.
+For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting.
[float]
[[resolve-migrations-failures]]
==== Resolving migration failures
-If {kib} terminates unexpectedly while migrating a saved object index, manual intervention is required before {kib} will attempt to perform the migration again. Follow the advice in (preventing migration failures)[preventing-migration-failures] before retrying a migration upgrade.
-
-As mentioned above, {kib} will create a migration lock for each index that requires a migration by creating a new `.kibana_N+1` index. For example: if the `.kibana_task_manager` alias is pointing to `.kibana_task_manager_5` then the first {kib} that succeeds in creating `.kibana_task_manager_6` will obtain the lock to start migrations.
-
-However, if the instance that obtained the lock fails to migrate the index, all other {kib} instances will be blocked from performing this migration. This includes the instance that originally obtained the lock, it will be blocked from retrying the migration even when restarted.
-
-[float]
-===== Retry a migration by restoring a backup snapshot:
-
-1. Before proceeding ensure that you have a recent and successful backup snapshot of all `.kibana*` indices.
-2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration.
-3. Delete all saved object indices with `DELETE /.kibana*`
-4. Restore the `.kibana* indices and their aliases from the backup snapshot. See {es} {ref}/modules-snapshots.html[snapshots]
-5. Start up all {kib} instances to retry the upgrade migration.
-
-[float]
-===== (Not recommended) Retry a migration without a backup snapshot:
+If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock.
-1. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration.
-2. Identify any migration locks by comparing the output of `GET /_cat/aliases` and `GET /_cat/indices`. If e.g. `.kibana` is pointing to `.kibana_4` and there is a `.kibana_5` index, the `.kibana_5` index will act like a migration lock blocking further attempts. Be sure to check both the `.kibana` and `.kibana_task_manager` aliases and their indices.
-3. Remove any migration locks e.g. `DELETE /.kibana_5`.
-4. Start up all {kib} instances.
+If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support.
[float]
[[upgrade-migrations-rolling-back]]
==== Rolling back to a previous version of {kib}
-If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify the root cause.
+If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause.
WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. +
Any changes made after an upgrade will be lost when rolling back to a previous version.
-In order to rollback after a failed upgrade migration, the saved object indices might also have to be rolled back to be compatible with the previous {kibana} version.
+In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version.
[float]
===== Rollback by restoring a backup snapshot:
@@ -111,17 +110,15 @@ In order to rollback after a failed upgrade migration, the saved object indices
[float]
===== (Not recommended) Rollback without a backup snapshot:
-WARNING: {kib} does not run a migration for every saved object index on every upgrade. A {kib} version upgrade can cause no migrations, migrate only the `.kibana` or the `.kibana_task_manager` index or both. Carefully read the logs to ensure that you're only deleting indices created by a later version of {kib} to avoid data loss.
-
1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration.
2. Create a backup snapshot of the `.kibana*` indices.
-3. Use the logs from the upgraded instances to identify which indices {kib} attempted to upgrade. The server logs will contain an entry like `[savedobjects-service] Creating index .kibana_4.` and/or `[savedobjects-service] Creating index .kibana_task_manager_2.` If no indices were created after upgrading {kib} then no further action is required to perform a rollback, skip ahead to step (5). If you're running multiple {kib} instances, be sure to inspect all instances' logs.
-4. Delete each of the indices identified in step (2). e.g. `DELETE /.kibana_task_manager_2`
-5. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`.
+3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*`
+4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`.
+5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}`
6. Start up {kib} on the older version you wish to rollback to.
[float]
[[upgrade-migrations-old-indices]]
==== Handling old `.kibana_N` indices
-After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, etc). {kib} only uses the index that the `.kibana` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version.
\ No newline at end of file
+After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version.
\ No newline at end of file
diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc
index b27bb8867e624..b43da6aef9765 100644
--- a/docs/setup/upgrade/upgrade-standard.asciidoc
+++ b/docs/setup/upgrade/upgrade-standard.asciidoc
@@ -15,17 +15,17 @@ necessary remediation steps as per those instructions.
[float]
==== Upgrading multiple {kib} instances
-WARNING: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading.
+NOTE: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading.
-Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause upgrade migration failures and data loss. This is because acknowledged writes from the older instances could be written into the _old_ index while the migration is in progress. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version.
-
-The first instance that triggers saved object migrations will run the entire process. Any other instances started up while a migration is running will log a message and then wait until saved object migrations has completed before they start serving HTTP traffic.
+Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause data loss. This is because older instances will continue to write saved objects in a different format than the newer instances. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version.
[float]
==== Upgrade using a `deb` or `rpm` package
. Stop the existing {kib} process using the appropriate command for your
- system. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss.
+ system. If you have multiple {kib} instances connecting to the same {es}
+ cluster ensure that all instances are stopped before proceeding to the next
+ step to avoid data loss.
. Use `rpm` or `dpkg` to install the new package. All files should be placed in
their proper locations and config files should not be overwritten.
+
@@ -65,5 +65,7 @@ and becomes a new instance in the monitoring data.
. Install the appropriate versions of all your plugins for your new
installation using the `kibana-plugin` script. Check out the
<> documentation for more information.
-. Stop the old {kib} process. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss.
+. Stop the old {kib} process. If you have multiple {kib} instances connecting
+ to the same {es} cluster ensure that all instances are stopped before
+ proceeding to the next step to avoid data loss.
. Start the new {kib} process.
diff --git a/examples/expressions_explorer/README.md b/examples/expressions_explorer/README.md
new file mode 100644
index 0000000000000..ead0ca758f8e5
--- /dev/null
+++ b/examples/expressions_explorer/README.md
@@ -0,0 +1,8 @@
+## expressions explorer
+
+This example expressions explorer app shows how to:
+ - to run expression
+ - to render expression output
+ - emit events from expression renderer and handle them
+
+To run this example, use the command `yarn start --run-examples`.
\ No newline at end of file
diff --git a/examples/expressions_explorer/kibana.json b/examples/expressions_explorer/kibana.json
new file mode 100644
index 0000000000000..038b7eea0ef21
--- /dev/null
+++ b/examples/expressions_explorer/kibana.json
@@ -0,0 +1,10 @@
+{
+ "id": "expressionsExplorer",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "server": false,
+ "ui": true,
+ "requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"],
+ "optionalPlugins": [],
+ "requiredBundles": []
+}
diff --git a/examples/expressions_explorer/public/actions/navigate_action.ts b/examples/expressions_explorer/public/actions/navigate_action.ts
new file mode 100644
index 0000000000000..d29a9e6b345b6
--- /dev/null
+++ b/examples/expressions_explorer/public/actions/navigate_action.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { createAction } from '../../../../src/plugins/ui_actions/public';
+
+export const ACTION_NAVIGATE = 'ACTION_NAVIGATE';
+
+export const createNavigateAction = () =>
+ createAction({
+ id: ACTION_NAVIGATE,
+ type: ACTION_NAVIGATE,
+ getDisplayName: () => 'Navigate',
+ execute: async (event: any) => {
+ window.location.href = event.href;
+ },
+ });
diff --git a/examples/expressions_explorer/public/actions/navigate_trigger.ts b/examples/expressions_explorer/public/actions/navigate_trigger.ts
new file mode 100644
index 0000000000000..eacbd968eaa93
--- /dev/null
+++ b/examples/expressions_explorer/public/actions/navigate_trigger.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { Trigger } from '../../../../src/plugins/ui_actions/public';
+
+export const NAVIGATE_TRIGGER_ID = 'NAVIGATE_TRIGGER_ID';
+
+export const navigateTrigger: Trigger = {
+ id: NAVIGATE_TRIGGER_ID,
+};
diff --git a/examples/expressions_explorer/public/actions_and_expressions.tsx b/examples/expressions_explorer/public/actions_and_expressions.tsx
new file mode 100644
index 0000000000000..6e2eebcde4a0f
--- /dev/null
+++ b/examples/expressions_explorer/public/actions_and_expressions.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiPanel,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import {
+ ExpressionsStart,
+ ReactExpressionRenderer,
+ ExpressionsInspectorAdapter,
+} from '../../../src/plugins/expressions/public';
+import { ExpressionEditor } from './editor/expression_editor';
+import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
+import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger';
+
+interface Props {
+ expressions: ExpressionsStart;
+ actions: UiActionsStart;
+}
+
+export function ActionsExpressionsExample({ expressions, actions }: Props) {
+ const [expression, updateExpression] = useState(
+ 'button name="click me" href="http://www.google.com"'
+ );
+
+ const expressionChanged = (value: string) => {
+ updateExpression(value);
+ };
+
+ const inspectorAdapters = {
+ expression: new ExpressionsInspectorAdapter(),
+ };
+
+ const handleEvents = (event: any) => {
+ if (event.id !== 'NAVIGATE') return;
+ // enrich event context with some extra data
+ event.baseUrl = 'http://www.google.com';
+
+ actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.value);
+ };
+
+ return (
+
+
+
+
+
Actions from expression renderers
+
+
+
+
+
+
+
+
+ Here you can play with sample `button` which takes a url as configuration and
+ displays a button which emits custom BUTTON_CLICK trigger to which we have attached
+ a custom action which performs the navigation.
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ return
{message}
;
+ }}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/examples/expressions_explorer/public/app.tsx b/examples/expressions_explorer/public/app.tsx
new file mode 100644
index 0000000000000..d72cf08128a5a
--- /dev/null
+++ b/examples/expressions_explorer/public/app.tsx
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {
+ EuiPage,
+ EuiPageHeader,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiSpacer,
+ EuiText,
+ EuiLink,
+} from '@elastic/eui';
+import { AppMountParameters } from '../../../src/core/public';
+import { ExpressionsStart } from '../../../src/plugins/expressions/public';
+import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
+import { RunExpressionsExample } from './run_expressions';
+import { RenderExpressionsExample } from './render_expressions';
+import { ActionsExpressionsExample } from './actions_and_expressions';
+import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
+
+interface Props {
+ expressions: ExpressionsStart;
+ inspector: InspectorStart;
+ actions: UiActionsStart;
+}
+
+const ExpressionsExplorer = ({ expressions, inspector, actions }: Props) => {
+ return (
+
+
+ Expressions Explorer
+
+
+
+
+ There are a couple of ways to run the expressions. Below some of the options are
+ demonstrated. You can read more about it{' '}
+
+ here
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const renderApp = (props: Props, { element }: AppMountParameters) => {
+ ReactDOM.render(, element);
+
+ return () => ReactDOM.unmountComponentAtNode(element);
+};
diff --git a/examples/expressions_explorer/public/editor/expression_editor.tsx b/examples/expressions_explorer/public/editor/expression_editor.tsx
new file mode 100644
index 0000000000000..e3dbb5998b92e
--- /dev/null
+++ b/examples/expressions_explorer/public/editor/expression_editor.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiCodeEditor } from '@elastic/eui';
+
+interface Props {
+ value: string;
+ onChange: (value: string) => void;
+}
+
+export function ExpressionEditor({ value, onChange }: Props) {
+ return (
+ {}}
+ aria-label="Code Editor"
+ />
+ );
+}
diff --git a/examples/expressions_explorer/public/functions/button.ts b/examples/expressions_explorer/public/functions/button.ts
new file mode 100644
index 0000000000000..8c39aa2743b30
--- /dev/null
+++ b/examples/expressions_explorer/public/functions/button.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/common';
+
+interface Arguments {
+ href: string;
+ name: string;
+}
+
+export type ExpressionFunctionButton = ExpressionFunctionDefinition<
+ 'button',
+ unknown,
+ Arguments,
+ unknown
+>;
+
+export const buttonFn: ExpressionFunctionButton = {
+ name: 'button',
+ args: {
+ href: {
+ help: i18n.translate('expressions.functions.font.args.href', {
+ defaultMessage: 'Link to which to navigate',
+ }),
+ types: ['string'],
+ required: true,
+ },
+ name: {
+ help: i18n.translate('expressions.functions.font.args.name', {
+ defaultMessage: 'Name of the button',
+ }),
+ types: ['string'],
+ default: 'button',
+ },
+ },
+ help: 'Configures the button',
+ fn: (input: unknown, args: Arguments) => {
+ return {
+ type: 'render',
+ as: 'button',
+ value: args,
+ };
+ },
+};
diff --git a/examples/expressions_explorer/public/index.ts b/examples/expressions_explorer/public/index.ts
new file mode 100644
index 0000000000000..a6dbbc9198f44
--- /dev/null
+++ b/examples/expressions_explorer/public/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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { ExpressionsExplorerPlugin } from './plugin';
+
+export const plugin = () => new ExpressionsExplorerPlugin();
diff --git a/examples/expressions_explorer/public/inspector/ast_debug_view.tsx b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx
new file mode 100644
index 0000000000000..d860ff30bd8e9
--- /dev/null
+++ b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { useState } from 'react';
+import { EuiTreeView, EuiDescriptionList, EuiCodeBlock, EuiText, EuiSpacer } from '@elastic/eui';
+
+interface Props {
+ ast: any;
+}
+
+const decorateAst = (ast: any, nodeClicked: any) => {
+ return ast.chain.map((link: any) => {
+ return {
+ id: link.function + Math.random(),
+ label: link.function,
+ callback: () => {
+ nodeClicked(link.debug);
+ },
+ children: Object.keys(link.arguments).reduce((result: any, key: string) => {
+ if (typeof link.arguments[key] === 'object') {
+ // result[key] = decorateAst(link.arguments[key]);
+ }
+ return result;
+ }, []),
+ };
+ });
+};
+
+const prepareNode = (key: string, value: any) => {
+ if (key === 'args') {
+ return (
+
+ {JSON.stringify(value, null, '\t')}
+
+ );
+ } else if (key === 'output' || key === 'input') {
+ return (
+
+ {JSON.stringify(value, null, '\t')}
+
+ );
+ } else if (key === 'success') {
+ return value ? 'true' : 'false';
+ } else return {value};
+};
+
+export function AstDebugView({ ast }: Props) {
+ const [nodeInfo, setNodeInfo] = useState([] as any[]);
+ const items = decorateAst(ast, (node: any) => {
+ setNodeInfo(
+ Object.keys(node).map((key) => ({
+ title: key,
+ description: prepareNode(key, node[key]),
+ }))
+ );
+ });
+
+ return (
+
+ List of executed expression functions:
+
+
+ Details of selected function:
+
+
+ );
+}
diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx
new file mode 100644
index 0000000000000..1233735072d04
--- /dev/null
+++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiEmptyPrompt } from '@elastic/eui';
+import { InspectorViewProps, Adapters } from '../../../../src/plugins/inspector/public';
+import { AstDebugView } from './ast_debug_view';
+
+interface ExpressionsInspectorViewComponentState {
+ ast: any;
+ adapters: Adapters;
+}
+
+class ExpressionsInspectorViewComponent extends Component<
+ InspectorViewProps,
+ ExpressionsInspectorViewComponentState
+> {
+ static propTypes = {
+ adapters: PropTypes.object.isRequired,
+ title: PropTypes.string.isRequired,
+ };
+
+ state = {} as ExpressionsInspectorViewComponentState;
+
+ static getDerivedStateFromProps(
+ nextProps: Readonly,
+ state: ExpressionsInspectorViewComponentState
+ ) {
+ if (state && nextProps.adapters === state.adapters) {
+ return null;
+ }
+
+ const { ast } = nextProps.adapters.expression;
+
+ return {
+ adapters: nextProps.adapters,
+ ast,
+ };
+ }
+
+ onUpdateData = (ast: any) => {
+ this.setState({
+ ast,
+ });
+ };
+
+ componentDidMount() {
+ this.props.adapters.expression!.on('change', this.onUpdateData);
+ }
+
+ componentWillUnmount() {
+ this.props.adapters.expression!.removeListener('change', this.onUpdateData);
+ }
+
+ static renderNoData() {
+ return (
+
+
+
+ }
+ body={
+
+
+
+
+
+ }
+ />
+ );
+ }
+
+ render() {
+ if (!this.state.ast) {
+ return ExpressionsInspectorViewComponent.renderNoData();
+ }
+
+ return ;
+ }
+}
+
+// default export required for React.Lazy
+// eslint-disable-next-line import/no-default-export
+export default ExpressionsInspectorViewComponent;
diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx
new file mode 100644
index 0000000000000..b10c82e5df309
--- /dev/null
+++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { lazy } from 'react';
+
+const ExpressionsInspectorViewComponent = lazy(() => import('./expressions_inspector_view'));
+
+export const getExpressionsInspectorViewComponentWrapper = () => {
+ return (props: any) => {
+ return ;
+ };
+};
diff --git a/examples/expressions_explorer/public/inspector/index.ts b/examples/expressions_explorer/public/inspector/index.ts
new file mode 100644
index 0000000000000..ec87a1240ac74
--- /dev/null
+++ b/examples/expressions_explorer/public/inspector/index.ts
@@ -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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { Adapters, InspectorViewDescription } from '../../../../src/plugins/inspector/public';
+import { getExpressionsInspectorViewComponentWrapper } from './expressions_inspector_view_wrapper';
+
+export const getExpressionsInspectorViewDescription = (): InspectorViewDescription => ({
+ title: i18n.translate('data.inspector.table.dataTitle', {
+ defaultMessage: 'Expression',
+ }),
+ order: 100,
+ help: i18n.translate('data.inspector.table..dataDescriptionTooltip', {
+ defaultMessage: 'View the expression behind the visualization',
+ }),
+ shouldShow(adapters: Adapters) {
+ return Boolean(adapters.expression);
+ },
+ component: getExpressionsInspectorViewComponentWrapper(),
+});
diff --git a/examples/expressions_explorer/public/plugin.tsx b/examples/expressions_explorer/public/plugin.tsx
new file mode 100644
index 0000000000000..9643389ad881c
--- /dev/null
+++ b/examples/expressions_explorer/public/plugin.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public';
+import { DeveloperExamplesSetup } from '../../developer_examples/public';
+import { ExpressionsSetup, ExpressionsStart } from '../../../src/plugins/expressions/public';
+import {
+ Setup as InspectorSetup,
+ Start as InspectorStart,
+} from '../../../src/plugins/inspector/public';
+import { getExpressionsInspectorViewDescription } from './inspector';
+import { UiActionsStart, UiActionsSetup } from '../../../src/plugins/ui_actions/public';
+import { NAVIGATE_TRIGGER_ID, navigateTrigger } from './actions/navigate_trigger';
+import { ACTION_NAVIGATE, createNavigateAction } from './actions/navigate_action';
+import { buttonRenderer } from './renderers/button';
+import { buttonFn } from './functions/button';
+
+interface StartDeps {
+ expressions: ExpressionsStart;
+ inspector: InspectorStart;
+ uiActions: UiActionsStart;
+}
+
+interface SetupDeps {
+ uiActions: UiActionsSetup;
+ expressions: ExpressionsSetup;
+ inspector: InspectorSetup;
+ developerExamples: DeveloperExamplesSetup;
+}
+
+export class ExpressionsExplorerPlugin implements Plugin {
+ public setup(core: CoreSetup, deps: SetupDeps) {
+ // register custom inspector adapter & view
+ deps.inspector.registerView(getExpressionsInspectorViewDescription());
+
+ // register custom actions
+ deps.uiActions.registerTrigger(navigateTrigger);
+ deps.uiActions.registerAction(createNavigateAction());
+ deps.uiActions.attachAction(NAVIGATE_TRIGGER_ID, ACTION_NAVIGATE);
+
+ // register custom functions and renderers
+ deps.expressions.registerRenderer(buttonRenderer);
+ deps.expressions.registerFunction(buttonFn);
+
+ core.application.register({
+ id: 'expressionsExplorer',
+ title: 'Expressions Explorer',
+ navLinkStatus: AppNavLinkStatus.hidden,
+ async mount(params: AppMountParameters) {
+ const [, depsStart] = await core.getStartServices();
+ const { renderApp } = await import('./app');
+ return renderApp(
+ {
+ expressions: depsStart.expressions,
+ inspector: depsStart.inspector,
+ actions: depsStart.uiActions,
+ },
+ params
+ );
+ },
+ });
+
+ deps.developerExamples.register({
+ appId: 'expressionsExplorer',
+ title: 'Expressions',
+ description: `Expressions is a plugin that allows to execute Kibana expressions and render content using expression renderers. This example plugin showcases various usage scenarios.`,
+ links: [
+ {
+ label: 'README',
+ href: 'https://github.com/elastic/kibana/blob/master/src/plugins/expressions/README.md',
+ iconType: 'logoGithub',
+ size: 's',
+ target: '_blank',
+ },
+ ],
+ });
+ }
+
+ public start() {}
+
+ public stop() {}
+}
diff --git a/examples/expressions_explorer/public/render_expressions.tsx b/examples/expressions_explorer/public/render_expressions.tsx
new file mode 100644
index 0000000000000..ffbe558f30218
--- /dev/null
+++ b/examples/expressions_explorer/public/render_expressions.tsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiPanel,
+ EuiText,
+ EuiTitle,
+ EuiButton,
+} from '@elastic/eui';
+import {
+ ExpressionsStart,
+ ReactExpressionRenderer,
+ ExpressionsInspectorAdapter,
+} from '../../../src/plugins/expressions/public';
+import { ExpressionEditor } from './editor/expression_editor';
+import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
+
+interface Props {
+ expressions: ExpressionsStart;
+ inspector: InspectorStart;
+}
+
+export function RenderExpressionsExample({ expressions, inspector }: Props) {
+ const [expression, updateExpression] = useState('markdown "## expressions explorer rendering"');
+
+ const expressionChanged = (value: string) => {
+ updateExpression(value);
+ };
+
+ const inspectorAdapters = {
+ expression: new ExpressionsInspectorAdapter(),
+ };
+
+ return (
+
+
+
+
+
Render expressions
+
+
+
+
+
+
+
+
+ In the below editor you can enter your expression and render it. Using
+ ReactExpressionRenderer component makes that very easy.
+
+
+
+ {
+ inspector.open(inspectorAdapters);
+ }}
+ >
+ Open Inspector
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ return
{message}
;
+ }}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/examples/expressions_explorer/public/renderers/button.tsx b/examples/expressions_explorer/public/renderers/button.tsx
new file mode 100644
index 0000000000000..32f1f31894dce
--- /dev/null
+++ b/examples/expressions_explorer/public/renderers/button.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import ReactDOM from 'react-dom';
+import React from 'react';
+import { EuiButton } from '@elastic/eui';
+import { ExpressionRenderDefinition } from '../../../../src/plugins/expressions/common/expression_renderers';
+
+export const buttonRenderer: ExpressionRenderDefinition = {
+ name: 'button',
+ displayName: 'Button',
+ reuseDomNode: true,
+ render(domNode, config, handlers) {
+ const buttonClick = () => {
+ handlers.event({
+ id: 'NAVIGATE',
+ value: {
+ href: config.href,
+ },
+ });
+ };
+
+ const renderDebug = () => (
+
+
+ {config.name}
+
+
+ );
+
+ ReactDOM.render(renderDebug(), domNode, () => handlers.done());
+
+ handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
+ },
+};
diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx
new file mode 100644
index 0000000000000..efbdbc2d41836
--- /dev/null
+++ b/examples/expressions_explorer/public/run_expressions.tsx
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ EuiCodeBlock,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiPanel,
+ EuiText,
+ EuiTitle,
+ EuiButton,
+} from '@elastic/eui';
+import {
+ ExpressionsStart,
+ ExpressionsInspectorAdapter,
+} from '../../../src/plugins/expressions/public';
+import { ExpressionEditor } from './editor/expression_editor';
+import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
+
+interface Props {
+ expressions: ExpressionsStart;
+ inspector: InspectorStart;
+}
+
+export function RunExpressionsExample({ expressions, inspector }: Props) {
+ const [expression, updateExpression] = useState('markdown "## expressions explorer"');
+ const [result, updateResult] = useState({});
+
+ const expressionChanged = (value: string) => {
+ updateExpression(value);
+ };
+
+ const inspectorAdapters = useMemo(
+ () => ({
+ expression: new ExpressionsInspectorAdapter(),
+ }),
+ []
+ );
+
+ useEffect(() => {
+ const runExpression = async () => {
+ const execution = expressions.execute(expression, null, {
+ debug: true,
+ inspectorAdapters,
+ });
+
+ const data: any = await execution.getData();
+ updateResult(data);
+ };
+
+ runExpression();
+ }, [expression, expressions, inspectorAdapters]);
+
+ return (
+
+
+
+
+
Run expressions
+
+
+
+
+
+
+
+
+ In the below editor you can enter your expression and execute it. Using
+ expressions.execute allows you to easily run the expression.
+
+
+
+ {
+ inspector.open(inspectorAdapters);
+ }}
+ >
+ Open Inspector
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {JSON.stringify(result, null, '\t')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/expressions_explorer/tsconfig.json b/examples/expressions_explorer/tsconfig.json
new file mode 100644
index 0000000000000..b4449819b25a6
--- /dev/null
+++ b/examples/expressions_explorer/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true
+ },
+ "include": [
+ "index.ts",
+ "public/**/*.ts",
+ "public/**/*.tsx",
+ "../../typings/**/*",
+ ],
+ "exclude": [],
+ "references": [
+ { "path": "../../src/core/tsconfig.json" },
+ { "path": "../../src/plugins/kibana_react/tsconfig.json" },
+ ]
+}
diff --git a/package.json b/package.json
index 87e0f84695235..24297011ccc63 100644
--- a/package.json
+++ b/package.json
@@ -287,7 +287,7 @@
"react-resizable": "^1.7.5",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
- "react-use": "^13.27.0",
+ "react-use": "^15.3.4",
"recompose": "^0.26.0",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
@@ -828,7 +828,7 @@
"url-loader": "^2.2.0",
"use-resize-observer": "^6.0.0",
"val-loader": "^1.1.1",
- "vega": "^5.18.0",
+ "vega": "^5.19.1",
"vega-lite": "^4.17.0",
"vega-schema-url-parser": "^2.1.0",
"vega-tooltip": "^0.25.0",
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
index 741f715ba6ebe..6ba652abda3d5 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
@@ -206,6 +206,20 @@ describe('DocumentMigrator', () => {
);
});
+ it('coerces the current Kibana version if it has a hyphen', () => {
+ const validDefinition = {
+ kibanaVersion: '3.2.0-SNAPSHOT',
+ typeRegistry: createRegistry({
+ name: 'foo',
+ convertToMultiNamespaceTypeVersion: '3.2.0',
+ namespaceType: 'multiple',
+ }),
+ minimumConvertVersion: '0.0.0',
+ log: mockLogger,
+ };
+ expect(() => new DocumentMigrator(validDefinition)).not.toThrowError();
+ });
+
it('validates convertToMultiNamespaceTypeVersion is not used on a patch version', () => {
const invalidDefinition = {
kibanaVersion: '3.2.3',
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts
index e4b89a949d3cf..e93586ec7ce4c 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts
@@ -159,10 +159,11 @@ export class DocumentMigrator implements VersionedTransformer {
*/
constructor({
typeRegistry,
- kibanaVersion,
+ kibanaVersion: rawKibanaVersion,
minimumConvertVersion = DEFAULT_MINIMUM_CONVERT_VERSION,
log,
}: DocumentMigratorOptions) {
+ const kibanaVersion = rawKibanaVersion.split('-')[0]; // coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) to a regular semver (x.y.z)
validateMigrationDefinition(typeRegistry, kibanaVersion, minimumConvertVersion);
this.documentMigratorOptions = { typeRegistry, kibanaVersion, log };
diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts
index 20865bea2f897..7c28db333cc83 100644
--- a/src/plugins/charts/public/static/utils/transform_click_event.ts
+++ b/src/plugins/charts/public/static/utils/transform_click_event.ts
@@ -30,6 +30,9 @@ export interface BrushTriggerEvent {
type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>;
+// TODO: replace when exported from elastic/charts
+const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__';
+
/**
* returns accessor value from string or function accessor
* @param datum
@@ -82,6 +85,29 @@ const getAllSplitAccessors = (
value,
]);
+/**
+ * Gets value from small multiple accessors
+ *
+ * Only handles single small multiple accessor
+ */
+function getSplitChartValue({
+ smHorizontalAccessorValue,
+ smVerticalAccessorValue,
+}: Pick):
+ | string
+ | number
+ | undefined {
+ if (smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) {
+ return smHorizontalAccessorValue;
+ }
+
+ if (smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) {
+ return smVerticalAccessorValue;
+ }
+
+ return;
+}
+
/**
* Reduces matching column indexes
*
@@ -92,7 +118,8 @@ const getAllSplitAccessors = (
const columnReducer = (
xAccessor: Accessor | AccessorFn | null,
yAccessor: Accessor | AccessorFn | null,
- splitAccessors: AllSeriesAccessors
+ splitAccessors: AllSeriesAccessors,
+ splitChartAccessor?: Accessor | AccessorFn
) => (
acc: Array<[index: number, id: string]>,
{ id }: Datatable['columns'][number],
@@ -101,6 +128,7 @@ const columnReducer = (
if (
(xAccessor !== null && validateAccessorId(id, xAccessor)) ||
(yAccessor !== null && validateAccessorId(id, yAccessor)) ||
+ (splitChartAccessor !== undefined && validateAccessorId(id, splitChartAccessor)) ||
splitAccessors.some(([accessor]) => validateAccessorId(id, accessor))
) {
acc.push([index, id]);
@@ -121,13 +149,18 @@ const rowFindPredicate = (
geometry: GeometryValue | null,
xAccessor: Accessor | AccessorFn | null,
yAccessor: Accessor | AccessorFn | null,
- splitAccessors: AllSeriesAccessors
+ splitAccessors: AllSeriesAccessors,
+ splitChartAccessor?: Accessor | AccessorFn,
+ splitChartValue?: string | number
) => (row: Datatable['rows'][number]): boolean =>
(geometry === null ||
(xAccessor !== null &&
getAccessorValue(row, xAccessor) === geometry.x &&
yAccessor !== null &&
- getAccessorValue(row, yAccessor) === geometry.y)) &&
+ getAccessorValue(row, yAccessor) === geometry.y &&
+ (splitChartAccessor === undefined ||
+ (splitChartValue !== undefined &&
+ getAccessorValue(row, splitChartAccessor) === splitChartValue)))) &&
[...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value);
/**
@@ -142,19 +175,28 @@ export const getFilterFromChartClickEventFn = (
table: Datatable,
xAccessor: Accessor | AccessorFn,
splitSeriesAccessorFnMap?: Map,
+ splitChartAccessor?: Accessor | AccessorFn,
negate: boolean = false
) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => {
const data: ValueClickContext['data']['data'] = [];
points.forEach((point) => {
const [geometry, { yAccessor, splitAccessors }] = point;
+ const splitChartValue = getSplitChartValue(point[1]);
const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap);
const columns = table.columns.reduce>(
- columnReducer(xAccessor, yAccessor, allSplitAccessors),
+ columnReducer(xAccessor, yAccessor, allSplitAccessors, splitChartAccessor),
[]
);
const row = table.rows.findIndex(
- rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors)
+ rowFindPredicate(
+ geometry,
+ xAccessor,
+ yAccessor,
+ allSplitAccessors,
+ splitChartAccessor,
+ splitChartValue
+ )
);
const newData = columns.map(([column, id]) => ({
table,
@@ -179,16 +221,20 @@ export const getFilterFromChartClickEventFn = (
* Helper function to get filter action event from series
*/
export const getFilterFromSeriesFn = (table: Datatable) => (
- { splitAccessors }: XYChartSeriesIdentifier,
+ { splitAccessors, ...rest }: XYChartSeriesIdentifier,
splitSeriesAccessorFnMap?: Map,
+ splitChartAccessor?: Accessor | AccessorFn,
negate = false
): ClickTriggerEvent => {
+ const splitChartValue = getSplitChartValue(rest);
const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap);
const columns = table.columns.reduce>(
- columnReducer(null, null, allSplitAccessors),
+ columnReducer(null, null, allSplitAccessors, splitChartAccessor),
[]
);
- const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors));
+ const row = table.rows.findIndex(
+ rowFindPredicate(null, null, null, allSplitAccessors, splitChartAccessor, splitChartValue)
+ );
const data: ValueClickContext['data']['data'] = columns.map(([column, id]) => ({
table,
column,
diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.test.ts b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts
new file mode 100644
index 0000000000000..56db5346b7c6c
--- /dev/null
+++ b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { dataPluginMock } from '../../../../data/public/mocks';
+import { createSessionRestorationDataProvider } from './session_restoration';
+import { getAppStateDefaults } from './get_app_state_defaults';
+import { getSavedDashboardMock } from '../test_helpers';
+import { SavedObjectTagDecoratorTypeGuard } from '../../../../saved_objects_tagging_oss/public';
+
+describe('createSessionRestorationDataProvider', () => {
+ const mockDataPlugin = dataPluginMock.createStartContract();
+ const searchSessionInfoProvider = createSessionRestorationDataProvider({
+ data: mockDataPlugin,
+ getAppState: () =>
+ getAppStateDefaults(
+ getSavedDashboardMock(),
+ false,
+ ((() => false) as unknown) as SavedObjectTagDecoratorTypeGuard
+ ),
+ getDashboardTitle: () => 'Dashboard',
+ getDashboardId: () => 'Id',
+ });
+
+ describe('session state', () => {
+ test('restoreState has sessionId and initialState has not', async () => {
+ const searchSessionId = 'id';
+ (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation(
+ () => searchSessionId
+ );
+ const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData();
+ expect(initialState.searchSessionId).toBeUndefined();
+ expect(restoreState.searchSessionId).toBe(searchSessionId);
+ });
+
+ test('restoreState has absoluteTimeRange', async () => {
+ const relativeTime = 'relativeTime';
+ const absoluteTime = 'absoluteTime';
+ (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation(
+ () => relativeTime
+ );
+ (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation(
+ () => absoluteTime
+ );
+ const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData();
+ expect(initialState.timeRange).toBe(relativeTime);
+ expect(restoreState.timeRange).toBe(absoluteTime);
+ });
+
+ test('restoreState has refreshInterval paused', async () => {
+ const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData();
+ expect(initialState.refreshInterval).toBeUndefined();
+ expect(restoreState.refreshInterval?.pause).toBe(true);
+ });
+ });
+});
diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts
index 60a0c56a63218..fb57f8caa5ce4 100644
--- a/src/plugins/dashboard/public/application/lib/session_restoration.ts
+++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts
@@ -21,8 +21,8 @@ export function createSessionRestorationDataProvider(deps: {
getUrlGeneratorData: async () => {
return {
urlGeneratorId: DASHBOARD_APP_URL_GENERATOR,
- initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }),
- restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }),
+ initialState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: false }),
+ restoreState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: true }),
};
},
};
@@ -32,20 +32,17 @@ function getUrlGeneratorState({
data,
getAppState,
getDashboardId,
- forceAbsoluteTime,
+ shouldRestoreSearchSession,
}: {
data: DataPublicPluginStart;
getAppState: () => DashboardAppState;
getDashboardId: () => string;
- /**
- * Can force time range from time filter to convert from relative to absolute time range
- */
- forceAbsoluteTime: boolean;
+ shouldRestoreSearchSession: boolean;
}): DashboardUrlGeneratorState {
const appState = getAppState();
return {
dashboardId: getDashboardId(),
- timeRange: forceAbsoluteTime
+ timeRange: shouldRestoreSearchSession
? data.query.timefilter.timefilter.getAbsoluteTime()
: data.query.timefilter.timefilter.getTime(),
filters: data.query.filterManager.getFilters(),
@@ -55,6 +52,12 @@ function getUrlGeneratorState({
preserveSavedFilters: false,
viewMode: appState.viewMode,
panels: getDashboardId() ? undefined : appState.panels,
- searchSessionId: data.search.session.getSessionId(),
+ searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined,
+ refreshInterval: shouldRestoreSearchSession
+ ? {
+ pause: true, // force pause refresh interval when restoring a session
+ value: 0,
+ }
+ : undefined,
};
}
diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx
index ff10feea46d47..b12de3f4496c5 100644
--- a/src/plugins/discover/public/application/angular/directives/histogram.tsx
+++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx
@@ -154,6 +154,10 @@ export class DiscoverHistogram extends Component xAxisFormatter.convert(value)}
/>
{
// show all the Rows
$scope.minimumVisibleRows = $scope.hits;
// delay scrolling to after the rows have been rendered
- const bottomMarker = $element.find('#discoverBottomMarker');
- $timeout(() => {
- bottomMarker.focus();
- // The anchor tag is not technically empty (it's a hack to make Safari scroll)
- // so the browser will show a highlight: remove the focus once scrolled
- $timeout(() => {
- bottomMarker.blur();
- }, 0);
- }, 0);
+ const bottomMarker = document.getElementById('discoverBottomMarker');
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+ while ($scope.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) {
+ await wait(50);
+ }
+ bottomMarker.focus();
+ await wait(50);
+ bottomMarker.blur();
};
$scope.newQuery = function () {
diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts
index 45e5e252e8361..809664de5f073 100644
--- a/src/plugins/discover/public/application/angular/discover_state.test.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.test.ts
@@ -101,8 +101,9 @@ describe('Test discover state with legacy migration', () => {
describe('createSearchSessionRestorationDataProvider', () => {
let mockSavedSearch: SavedSearch = ({} as unknown) as SavedSearch;
+ const mockDataPlugin = dataPluginMock.createStartContract();
const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({
- data: dataPluginMock.createStartContract(),
+ data: mockDataPlugin,
appStateContainer: getState({
history: createBrowserHistory(),
}).appStateContainer,
@@ -124,4 +125,30 @@ describe('createSearchSessionRestorationDataProvider', () => {
expect(await searchSessionInfoProvider.getName()).toBe('Discover');
});
});
+
+ describe('session state', () => {
+ test('restoreState has sessionId and initialState has not', async () => {
+ const searchSessionId = 'id';
+ (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation(
+ () => searchSessionId
+ );
+ const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData();
+ expect(initialState.searchSessionId).toBeUndefined();
+ expect(restoreState.searchSessionId).toBe(searchSessionId);
+ });
+
+ test('restoreState has absoluteTimeRange', async () => {
+ const relativeTime = 'relativeTime';
+ const absoluteTime = 'absoluteTime';
+ (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation(
+ () => relativeTime
+ );
+ (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation(
+ () => absoluteTime
+ );
+ const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData();
+ expect(initialState.timeRange).toBe(relativeTime);
+ expect(restoreState.timeRange).toBe(absoluteTime);
+ });
+ });
});
diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts
index fe05fceb858e5..c769e263655ab 100644
--- a/src/plugins/discover/public/application/angular/discover_state.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.ts
@@ -275,12 +275,12 @@ export function createSearchSessionRestorationDataProvider(deps: {
initialState: createUrlGeneratorState({
...deps,
getSavedSearchId,
- forceAbsoluteTime: false,
+ shouldRestoreSearchSession: false,
}),
restoreState: createUrlGeneratorState({
...deps,
getSavedSearchId,
- forceAbsoluteTime: true,
+ shouldRestoreSearchSession: true,
}),
};
},
@@ -291,15 +291,12 @@ function createUrlGeneratorState({
appStateContainer,
data,
getSavedSearchId,
- forceAbsoluteTime,
+ shouldRestoreSearchSession,
}: {
appStateContainer: StateContainer;
data: DataPublicPluginStart;
getSavedSearchId: () => string | undefined;
- /**
- * Can force time range from time filter to convert from relative to absolute time range
- */
- forceAbsoluteTime: boolean;
+ shouldRestoreSearchSession: boolean;
}): DiscoverUrlGeneratorState {
const appState = appStateContainer.get();
return {
@@ -307,10 +304,10 @@ function createUrlGeneratorState({
indexPatternId: appState.index,
query: appState.query,
savedSearchId: getSavedSearchId(),
- timeRange: forceAbsoluteTime
+ timeRange: shouldRestoreSearchSession
? data.query.timefilter.timefilter.getAbsoluteTime()
: data.query.timefilter.timefilter.getTime(),
- searchSessionId: data.search.session.getSessionId(),
+ searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined,
columns: appState.columns,
sort: appState.sort,
savedQuery: appState.savedQuery,
diff --git a/src/plugins/discover/public/application/angular/helpers/point_series.ts b/src/plugins/discover/public/application/angular/helpers/point_series.ts
index db58aebfd3ad7..89317f77ef060 100644
--- a/src/plugins/discover/public/application/angular/helpers/point_series.ts
+++ b/src/plugins/discover/public/application/angular/helpers/point_series.ts
@@ -62,6 +62,7 @@ export interface Chart {
}>;
xAxisOrderedValues: number[];
xAxisFormat: Dimension['format'];
+ yAxisFormat: Dimension['format'];
xAxisLabel: Column['name'];
yAxisLabel?: Column['name'];
ordered: Ordered;
@@ -76,7 +77,7 @@ export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => {
chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number));
chart.xAxisFormat = x.format;
chart.xAxisLabel = table.columns[x.accessor].name;
-
+ chart.yAxisFormat = y.format;
const { intervalESUnit, intervalESValue, interval, bounds } = x.params;
chart.ordered = {
date: true,
diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts
index 8e068818ec0ce..0240ec90cb1e6 100644
--- a/src/plugins/expressions/common/execution/execution.ts
+++ b/src/plugins/expressions/common/execution/execution.ts
@@ -29,6 +29,7 @@ import { getByAlias } from '../util/get_by_alias';
import { ExecutionContract } from './execution_contract';
import { ExpressionExecutionParams } from '../service';
import { TablesAdapter } from '../util/tables_adapter';
+import { ExpressionsInspectorAdapter } from '../util/expressions_inspector_adapter';
/**
* AbortController is not available in Node until v15, so we
@@ -63,6 +64,7 @@ export interface ExecutionParams {
const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({
requests: new RequestAdapter(),
tables: new TablesAdapter(),
+ expression: new ExpressionsInspectorAdapter(),
});
export class Execution<
@@ -208,6 +210,9 @@ export class Execution<
this.firstResultFuture.promise
.then(
(result) => {
+ if (this.context.inspectorAdapters.expression) {
+ this.context.inspectorAdapters.expression.logAST(this.state.get().ast);
+ }
this.state.transitions.setResult(result);
},
(error) => {
diff --git a/src/plugins/expressions/common/util/expressions_inspector_adapter.ts b/src/plugins/expressions/common/util/expressions_inspector_adapter.ts
new file mode 100644
index 0000000000000..c82884d373d2f
--- /dev/null
+++ b/src/plugins/expressions/common/util/expressions_inspector_adapter.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { EventEmitter } from 'events';
+
+export class ExpressionsInspectorAdapter extends EventEmitter {
+ private _ast: any = {};
+
+ public logAST(ast: any): void {
+ this._ast = ast;
+ this.emit('change', this._ast);
+ }
+
+ public get ast() {
+ return this._ast;
+ }
+}
diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts
index ecb7d5cdca81e..4762f9979fe4a 100644
--- a/src/plugins/expressions/common/util/index.ts
+++ b/src/plugins/expressions/common/util/index.ts
@@ -9,3 +9,4 @@
export * from './create_error';
export * from './get_by_alias';
export * from './tables_adapter';
+export * from './expressions_inspector_adapter';
diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts
index 9485daf49c981..d6dd2fc1f3d37 100644
--- a/src/plugins/expressions/public/index.ts
+++ b/src/plugins/expressions/public/index.ts
@@ -107,4 +107,5 @@ export {
ExpressionsServiceSetup,
ExpressionsServiceStart,
TablesAdapter,
+ ExpressionsInspectorAdapter,
} from '../common';
diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md
index 7fa0857be8aba..029d727e82e74 100644
--- a/src/plugins/expressions/public/public.api.md
+++ b/src/plugins/expressions/public/public.api.md
@@ -551,6 +551,16 @@ export class ExpressionRenderHandler {
update$: Observable;
}
+// Warning: (ae-missing-release-tag) "ExpressionsInspectorAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export class ExpressionsInspectorAdapter extends EventEmitter {
+ // (undocumented)
+ get ast(): any;
+ // (undocumented)
+ logAST(ast: any): void;
+}
+
// Warning: (ae-missing-release-tag) "ExpressionsPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
diff --git a/src/plugins/saved_objects_management/public/lib/create_field_list.ts b/src/plugins/saved_objects_management/public/lib/create_field_list.ts
index cd30a02bd0ef3..4497fb04ffa2c 100644
--- a/src/plugins/saved_objects_management/public/lib/create_field_list.ts
+++ b/src/plugins/saved_objects_management/public/lib/create_field_list.ts
@@ -11,11 +11,12 @@ import { SimpleSavedObject } from '../../../../core/public';
import { castEsToKbnFieldTypeName } from '../../../data/public';
import { ObjectField } from '../management_section/types';
import { SavedObjectLoader } from '../../../saved_objects/public';
+import { SavedObjectWithMetadata } from '../types';
const maxRecursiveIterations = 20;
export function createFieldList(
- object: SimpleSavedObject,
+ object: SimpleSavedObject | SavedObjectWithMetadata,
service?: SavedObjectLoader
): ObjectField[] {
let fields = Object.entries(object.attributes as Record).reduce(
diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx
index 96a4a24f6591e..e048b92b9566c 100644
--- a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx
@@ -19,14 +19,15 @@ import { set } from '@elastic/safer-lodash-set';
import { cloneDeep } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public';
+import { SavedObjectsClientContract } from '../../../../../../core/public';
import { SavedObjectLoader } from '../../../../../saved_objects/public';
import { Field } from './field';
import { ObjectField, FieldState, SubmittedFormData } from '../../types';
import { createFieldList } from '../../../lib';
+import { SavedObjectWithMetadata } from '../../../types';
interface FormProps {
- object: SimpleSavedObject;
+ object: SavedObjectWithMetadata;
service: SavedObjectLoader;
savedObjectsClient: SavedObjectsClientContract;
editionEnabled: boolean;
diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx
index 31c0a76e16f58..3343e0a63f54c 100644
--- a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx
@@ -14,16 +14,18 @@ import {
SavedObjectsClientContract,
OverlayStart,
NotificationsStart,
- SimpleSavedObject,
ScopedHistory,
+ HttpSetup,
} from '../../../../../core/public';
import { ISavedObjectsManagementServiceRegistry } from '../../services';
import { Header, NotFoundErrors, Intro, Form } from './components';
-import { canViewInApp } from '../../lib';
+import { canViewInApp, findObject } from '../../lib';
import { SubmittedFormData } from '../types';
+import { SavedObjectWithMetadata } from '../../types';
interface SavedObjectEditionProps {
id: string;
+ http: HttpSetup;
serviceName: string;
serviceRegistry: ISavedObjectsManagementServiceRegistry;
capabilities: Capabilities;
@@ -36,7 +38,7 @@ interface SavedObjectEditionProps {
interface SavedObjectEditionState {
type: string;
- object?: SimpleSavedObject;
+ object?: SavedObjectWithMetadata;
}
export class SavedObjectEdition extends Component<
@@ -56,9 +58,9 @@ export class SavedObjectEdition extends Component<
}
componentDidMount() {
- const { id, savedObjectsClient } = this.props;
+ const { http, id } = this.props;
const { type } = this.state;
- savedObjectsClient.get(type, id).then((object) => {
+ findObject(http, type, id).then((object) => {
this.setState({
object,
});
@@ -70,7 +72,7 @@ export class SavedObjectEdition extends Component<
capabilities,
notFoundType,
serviceRegistry,
- id,
+ http,
serviceName,
savedObjectsClient,
} = this.props;
@@ -80,7 +82,7 @@ export class SavedObjectEdition extends Component<
string,
boolean
>;
- const canView = canViewInApp(capabilities, type);
+ const canView = canViewInApp(capabilities, type) && Boolean(object?.meta.inAppUrl?.path);
const service = serviceRegistry.get(serviceName)!.service;
return (
@@ -91,7 +93,7 @@ export class SavedObjectEdition extends Component<
canViewInApp={canView}
type={type}
onDeleteClick={() => this.delete()}
- viewUrl={service.urlFor(id)}
+ viewUrl={http.basePath.prepend(object?.meta.inAppUrl?.path || '')}
/>
{notFoundType && (
<>
diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx
index 758789aa0f47e..2af7c22488c51 100644
--- a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx
@@ -11,6 +11,7 @@ import { useParams, useLocation } from 'react-router-dom';
import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
import { CoreStart, ChromeBreadcrumb, ScopedHistory } from 'src/core/public';
+import { RedirectAppLinks } from '../../../kibana_react/public';
import { ISavedObjectsManagementServiceRegistry } from '../services';
import { SavedObjectEdition } from './object_view';
@@ -50,17 +51,20 @@ const SavedObjectsEditionPage = ({
}, [setBreadcrumbs, service]);
return (
-
+
+
+
);
};
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 50a08d96de951..27d9b5ce83203 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -5247,6 +5247,36 @@
}
}
},
+ "vis_type_table": {
+ "properties": {
+ "total": {
+ "type": "long"
+ },
+ "total_split": {
+ "type": "long"
+ },
+ "split_columns": {
+ "properties": {
+ "total": {
+ "type": "long"
+ },
+ "enabled": {
+ "type": "long"
+ }
+ }
+ },
+ "split_rows": {
+ "properties": {
+ "total": {
+ "type": "long"
+ },
+ "enabled": {
+ "type": "long"
+ }
+ }
+ }
+ }
+ },
"vis_type_vega": {
"properties": {
"vega_lib_specs_total": {
diff --git a/src/plugins/vis_type_table/common/index.ts b/src/plugins/vis_type_table/common/index.ts
new file mode 100644
index 0000000000000..cc54db82d37e7
--- /dev/null
+++ b/src/plugins/vis_type_table/common/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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+export * from './types';
diff --git a/src/plugins/vis_type_table/common/types.ts b/src/plugins/vis_type_table/common/types.ts
new file mode 100644
index 0000000000000..3380e730770c3
--- /dev/null
+++ b/src/plugins/vis_type_table/common/types.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+export const VIS_TYPE_TABLE = 'table';
+
+export enum AggTypes {
+ SUM = 'sum',
+ AVG = 'avg',
+ MIN = 'min',
+ MAX = 'max',
+ COUNT = 'count',
+}
+
+export interface TableVisParams {
+ perPage: number | '';
+ showPartialRows: boolean;
+ showMetricsAtAllLevels: boolean;
+ showToolbar: boolean;
+ showTotal: boolean;
+ totalFunc: AggTypes;
+ percentageCol: string;
+ row?: boolean;
+}
diff --git a/src/plugins/vis_type_table/jest.config.js b/src/plugins/vis_type_table/jest.config.js
index 4e5ddbcf8d7c5..3a7906f6ec543 100644
--- a/src/plugins/vis_type_table/jest.config.js
+++ b/src/plugins/vis_type_table/jest.config.js
@@ -11,4 +11,5 @@ module.exports = {
rootDir: '../../..',
roots: ['/src/plugins/vis_type_table'],
testRunner: 'jasmine2',
+ collectCoverageFrom: ['/src/plugins/vis_type_table/**/*.{js,ts,tsx}'],
};
diff --git a/src/plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx
index eb76659a601d6..a70ecb43f1be7 100644
--- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx
+++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx
@@ -19,7 +19,7 @@ import {
NumberInputOption,
VisOptionsProps,
} from '../../../vis_default_editor/public';
-import { TableVisParams } from '../types';
+import { TableVisParams } from '../../common';
import { totalAggregations } from './utils';
const { tabifyGetColumns } = search;
diff --git a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx
index fb0044a986f5e..716b77e9c91d2 100644
--- a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx
+++ b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx
@@ -9,7 +9,7 @@
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { VisOptionsProps } from 'src/plugins/vis_default_editor/public';
-import { TableVisParams } from '../types';
+import { TableVisParams } from '../../common';
const TableOptionsComponent = lazy(() => import('./table_vis_options'));
diff --git a/src/plugins/vis_type_table/public/components/utils.ts b/src/plugins/vis_type_table/public/components/utils.ts
index f11d7bc4b7f33..8f30788c76468 100644
--- a/src/plugins/vis_type_table/public/components/utils.ts
+++ b/src/plugins/vis_type_table/public/components/utils.ts
@@ -7,7 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { AggTypes } from '../types';
+import { AggTypes } from '../../common';
const totalAggregations = [
{
diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts
index cec16eefb360c..db0b92154d2dd 100644
--- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts
+++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts
@@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, Datatable, Render } from 'src/plugins/expressions/public';
import { tableVisLegacyResponseHandler, TableContext } from './table_vis_legacy_response_handler';
import { TableVisConfig } from '../types';
+import { VIS_TYPE_TABLE } from '../../common';
export type Input = Datatable;
@@ -19,7 +20,7 @@ interface Arguments {
export interface TableVisRenderValue {
visData: TableContext;
- visType: 'table';
+ visType: typeof VIS_TYPE_TABLE;
visConfig: TableVisConfig;
}
@@ -53,7 +54,7 @@ export const createTableVisLegacyFn = (): TableExpressionFunctionDefinition => (
as: 'table_vis',
value: {
visData: convertedData,
- visType: 'table',
+ visType: VIS_TYPE_TABLE,
visConfig,
},
};
diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts
index a1ceee8c741d4..3e1140275593d 100644
--- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts
+++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts
@@ -12,11 +12,11 @@ import { BaseVisTypeOptions } from '../../../visualizations/public';
import { TableOptions } from '../components/table_vis_options_lazy';
import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public';
+import { TableVisParams, VIS_TYPE_TABLE } from '../../common';
import { toExpressionAst } from '../to_ast';
-import { TableVisParams } from '../types';
export const tableVisLegacyTypeDefinition: BaseVisTypeOptions = {
- name: 'table',
+ name: VIS_TYPE_TABLE,
title: i18n.translate('visTypeTable.tableVisTitle', {
defaultMessage: 'Data table',
}),
diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts
index a45f1e828fc47..99fee424b8bea 100644
--- a/src/plugins/vis_type_table/public/table_vis_fn.ts
+++ b/src/plugins/vis_type_table/public/table_vis_fn.ts
@@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { tableVisResponseHandler, TableContext } from './table_vis_response_handler';
import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
import { TableVisConfig } from './types';
+import { VIS_TYPE_TABLE } from '../common';
export type Input = Datatable;
@@ -19,7 +20,7 @@ interface Arguments {
export interface TableVisRenderValue {
visData: TableContext;
- visType: 'table';
+ visType: typeof VIS_TYPE_TABLE;
visConfig: TableVisConfig;
}
@@ -56,7 +57,7 @@ export const createTableVisFn = (): TableExpressionFunctionDefinition => ({
as: 'table_vis',
value: {
visData: convertedData,
- visType: 'table',
+ visType: VIS_TYPE_TABLE,
visConfig,
},
};
diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts
index 8cd45b54c6ced..ef6d85db103b3 100644
--- a/src/plugins/vis_type_table/public/table_vis_type.ts
+++ b/src/plugins/vis_type_table/public/table_vis_type.ts
@@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n';
import { AggGroupNames } from '../../data/public';
import { BaseVisTypeOptions } from '../../visualizations/public';
+import { TableVisParams, VIS_TYPE_TABLE } from '../common';
import { TableOptions } from './components/table_vis_options_lazy';
import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
import { toExpressionAst } from './to_ast';
-import { TableVisParams } from './types';
export const tableVisTypeDefinition: BaseVisTypeOptions = {
- name: 'table',
+ name: VIS_TYPE_TABLE,
title: i18n.translate('visTypeTable.tableVisTitle', {
defaultMessage: 'Data table',
}),
diff --git a/src/plugins/vis_type_table/public/to_ast.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts
index 1ca62475b7af0..f0aed7199a2f2 100644
--- a/src/plugins/vis_type_table/public/to_ast.test.ts
+++ b/src/plugins/vis_type_table/public/to_ast.test.ts
@@ -8,7 +8,7 @@
import { Vis } from 'src/plugins/visualizations/public';
import { toExpressionAst } from './to_ast';
-import { AggTypes, TableVisParams } from './types';
+import { AggTypes, TableVisParams } from '../common';
const mockSchemas = {
metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }],
diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts
index 9d9f23d31d802..1cbe9832e4c98 100644
--- a/src/plugins/vis_type_table/public/to_ast.ts
+++ b/src/plugins/vis_type_table/public/to_ast.ts
@@ -12,8 +12,9 @@ import {
} from '../../data/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public';
+import { TableVisParams } from '../common';
import { TableExpressionFunctionDefinition } from './table_vis_fn';
-import { TableVisConfig, TableVisParams } from './types';
+import { TableVisConfig } from './types';
const buildTableVisConfig = (
schemas: ReturnType,
diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts
index 75d48f4f53ac7..03cf8bb3395d6 100644
--- a/src/plugins/vis_type_table/public/types.ts
+++ b/src/plugins/vis_type_table/public/types.ts
@@ -8,14 +8,7 @@
import { IFieldFormat } from 'src/plugins/data/public';
import { SchemaConfig } from 'src/plugins/visualizations/public';
-
-export enum AggTypes {
- SUM = 'sum',
- AVG = 'avg',
- MIN = 'min',
- MAX = 'max',
- COUNT = 'count',
-}
+import { TableVisParams } from '../common';
export interface Dimensions {
buckets: SchemaConfig[];
@@ -44,16 +37,6 @@ export interface TableVisUseUiStateProps {
setColumnsWidth: (column: ColumnWidthData) => void;
}
-export interface TableVisParams {
- perPage: number | '';
- showPartialRows: boolean;
- showMetricsAtAllLevels: boolean;
- showToolbar: boolean;
- showTotal: boolean;
- totalFunc: AggTypes;
- percentageCol: string;
-}
-
export interface TableVisConfig extends TableVisParams {
title: string;
dimensions: Dimensions;
diff --git a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts
index 5398aa908f6eb..3a733e7a9a4dc 100644
--- a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts
+++ b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts
@@ -9,8 +9,9 @@
import { useMemo } from 'react';
import { chain, findIndex } from 'lodash';
+import { AggTypes } from '../../../common';
import { Table } from '../../table_vis_response_handler';
-import { FormattedColumn, TableVisConfig, AggTypes } from '../../types';
+import { FormattedColumn, TableVisConfig } from '../../types';
import { getFormatService } from '../../services';
import { addPercentageColumn } from '../add_percentage_column';
diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts
index 1573a3c6b7b88..7e55e63f9249c 100644
--- a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts
+++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts
@@ -7,7 +7,7 @@
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { TableVisParams } from '../../types';
+import { TableVisParams } from '../../../common';
export const usePagination = (visParams: TableVisParams, rowCount: number) => {
const [pagination, setPagination] = useState({
diff --git a/src/plugins/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts
index 75068c646f501..39618d687168e 100644
--- a/src/plugins/vis_type_table/server/index.ts
+++ b/src/plugins/vis_type_table/server/index.ts
@@ -6,9 +6,11 @@
* Public License, v 1.
*/
-import { PluginConfigDescriptor } from 'kibana/server';
+import { CoreSetup, PluginConfigDescriptor } from 'kibana/server';
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { configSchema, ConfigSchema } from '../config';
+import { registerVisTypeTableUsageCollector } from './usage_collector';
export const config: PluginConfigDescriptor = {
exposeToBrowser: {
@@ -21,6 +23,10 @@ export const config: PluginConfigDescriptor = {
};
export const plugin = () => ({
- setup() {},
+ setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) {
+ if (plugins.usageCollection) {
+ registerVisTypeTableUsageCollector(plugins.usageCollection);
+ }
+ },
start() {},
});
diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts
new file mode 100644
index 0000000000000..55daa5c64349a
--- /dev/null
+++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SavedObjectsClientContract } from 'kibana/server';
+import { getStats } from './get_stats';
+
+const mockVisualizations = {
+ saved_objects: [
+ {
+ attributes: {
+ visState:
+ '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": true }], "params": { "row": true }}',
+ },
+ },
+ {
+ attributes: {
+ visState:
+ '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": false }], "params": { "row": true }}',
+ },
+ },
+ {
+ attributes: {
+ visState:
+ '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "split", "enabled": true }], "params": { "row": false }}',
+ },
+ },
+ {
+ attributes: {
+ visState: '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }]}',
+ },
+ },
+ {
+ attributes: { visState: '{"type": "histogram"}' },
+ },
+ ],
+};
+
+describe('vis_type_table getStats', () => {
+ const mockSoClient = ({
+ find: jest.fn().mockResolvedValue(mockVisualizations),
+ } as unknown) as SavedObjectsClientContract;
+
+ test('Returns stats from saved objects for table vis only', async () => {
+ const result = await getStats(mockSoClient);
+ expect(mockSoClient.find).toHaveBeenCalledWith({
+ type: 'visualization',
+ perPage: 10000,
+ });
+ expect(result).toEqual({
+ total: 4,
+ total_split: 3,
+ split_columns: {
+ total: 1,
+ enabled: 1,
+ },
+ split_rows: {
+ total: 2,
+ enabled: 1,
+ },
+ });
+ });
+});
diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts
new file mode 100644
index 0000000000000..bd3e1d2f089e2
--- /dev/null
+++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts
@@ -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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server';
+import {
+ SavedVisState,
+ VisualizationSavedObjectAttributes,
+} from 'src/plugins/visualizations/common';
+import { TableVisParams, VIS_TYPE_TABLE } from '../../common';
+
+export interface VisTypeTableUsage {
+ /**
+ * Total number of table type visualizations
+ */
+ total: number;
+ /**
+ * Total number of table visualizations, using "Split table" agg
+ */
+ total_split: number;
+ /**
+ * Split table by columns stats
+ */
+ split_columns: {
+ total: number;
+ enabled: number;
+ };
+ /**
+ * Split table by rows stats
+ */
+ split_rows: {
+ total: number;
+ enabled: number;
+ };
+}
+
+/*
+ * Parse the response data into telemetry payload
+ */
+export async function getStats(
+ soClient: SavedObjectsClientContract | ISavedObjectsRepository
+): Promise {
+ const visualizations = await soClient.find({
+ type: 'visualization',
+ perPage: 10000,
+ });
+
+ const tableVisualizations = visualizations.saved_objects
+ .map>(({ attributes }) => JSON.parse(attributes.visState))
+ .filter(({ type }) => type === VIS_TYPE_TABLE);
+
+ const defaultStats = {
+ total: tableVisualizations.length,
+ total_split: 0,
+ split_columns: {
+ total: 0,
+ enabled: 0,
+ },
+ split_rows: {
+ total: 0,
+ enabled: 0,
+ },
+ };
+
+ return tableVisualizations.reduce((acc, { aggs, params }) => {
+ const hasSplitAgg = aggs.find((agg) => agg.schema === 'split');
+
+ if (hasSplitAgg) {
+ acc.total_split += 1;
+
+ const isSplitRow = params.row;
+ const isSplitEnabled = hasSplitAgg.enabled;
+
+ const container = isSplitRow ? acc.split_rows : acc.split_columns;
+ container.total += 1;
+ container.enabled = isSplitEnabled ? container.enabled + 1 : container.enabled;
+ }
+
+ return acc;
+ }, defaultStats);
+}
diff --git a/src/plugins/vis_type_table/server/usage_collector/index.ts b/src/plugins/vis_type_table/server/usage_collector/index.ts
new file mode 100644
index 0000000000000..090ed3077b27c
--- /dev/null
+++ b/src/plugins/vis_type_table/server/usage_collector/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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+export { registerVisTypeTableUsageCollector } from './register_usage_collector';
diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts
new file mode 100644
index 0000000000000..cbf39a4d937a7
--- /dev/null
+++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+jest.mock('./get_stats', () => ({
+ getStats: jest.fn().mockResolvedValue({ somestat: 1 }),
+}));
+
+import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock';
+import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks';
+
+import { registerVisTypeTableUsageCollector } from './register_usage_collector';
+import { getStats } from './get_stats';
+
+describe('registerVisTypeTableUsageCollector', () => {
+ it('Usage collector configs fit the shape', () => {
+ const mockCollectorSet = createUsageCollectionSetupMock();
+ registerVisTypeTableUsageCollector(mockCollectorSet);
+ expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1);
+ expect(mockCollectorSet.registerCollector).toBeCalledTimes(1);
+ expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({
+ type: 'vis_type_table',
+ isReady: expect.any(Function),
+ fetch: expect.any(Function),
+ schema: {
+ total: { type: 'long' },
+ total_split: { type: 'long' },
+ split_columns: {
+ total: { type: 'long' },
+ enabled: { type: 'long' },
+ },
+ split_rows: {
+ total: { type: 'long' },
+ enabled: { type: 'long' },
+ },
+ },
+ });
+ const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
+ expect(usageCollectorConfig.isReady()).toBe(true);
+ });
+
+ it('Usage collector config.fetch calls getStats', async () => {
+ const mockCollectorSet = createUsageCollectionSetupMock();
+ registerVisTypeTableUsageCollector(mockCollectorSet);
+ const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value;
+ const mockCollectorFetchContext = createCollectorFetchContextMock();
+ const fetchResult = await usageCollector.fetch(mockCollectorFetchContext);
+ expect(getStats).toBeCalledTimes(1);
+ expect(getStats).toBeCalledWith(mockCollectorFetchContext.soClient);
+ expect(fetchResult).toEqual({ somestat: 1 });
+ });
+});
diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts
new file mode 100644
index 0000000000000..2ac4ce22a47e4
--- /dev/null
+++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts
@@ -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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+
+import { getStats, VisTypeTableUsage } from './get_stats';
+
+export function registerVisTypeTableUsageCollector(collectorSet: UsageCollectionSetup) {
+ const collector = collectorSet.makeUsageCollector({
+ type: 'vis_type_table',
+ isReady: () => true,
+ schema: {
+ total: { type: 'long' },
+ total_split: { type: 'long' },
+ split_columns: {
+ total: { type: 'long' },
+ enabled: { type: 'long' },
+ },
+ split_rows: {
+ total: { type: 'long' },
+ enabled: { type: 'long' },
+ },
+ },
+ fetch: ({ soClient }) => getStats(soClient),
+ });
+ collectorSet.registerCollector(collector);
+}
diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json
index bda86d06c0ff7..ccff3c349cf21 100644
--- a/src/plugins/vis_type_table/tsconfig.json
+++ b/src/plugins/vis_type_table/tsconfig.json
@@ -8,6 +8,7 @@
"declarationMap": true
},
"include": [
+ "common/**/*",
"public/**/*",
"server/**/*",
"*.ts"
diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js
index 157523cdf09f4..ee9bed141fe4b 100644
--- a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js
@@ -7,7 +7,7 @@
*/
import d3 from 'd3';
-import _ from 'lodash';
+import { isNumber, reduce, times } from 'lodash';
import moment from 'moment';
import { InvalidLogScaleValues } from '../../errors';
@@ -62,7 +62,7 @@ export class AxisScale {
return d3[extent](
opts.reduce(function (opts, v) {
- if (!_.isNumber(v)) v = +v;
+ if (!isNumber(v)) v = +v;
if (!isNaN(v)) opts.push(v);
return opts;
}, [])
@@ -90,7 +90,7 @@ export class AxisScale {
const y = moment(x);
const method = n > 0 ? 'add' : 'subtract';
- _.times(Math.abs(n), function () {
+ times(Math.abs(n), function () {
y[method](interval);
});
@@ -100,7 +100,7 @@ export class AxisScale {
getAllPoints() {
const config = this.axisConfig;
const data = this.visConfig.data.chartData();
- const chartPoints = _.reduce(
+ const chartPoints = reduce(
data,
(chartPoints, chart, chartIndex) => {
const points = chart.series.reduce((points, seri, seriIndex) => {
@@ -254,6 +254,6 @@ export class AxisScale {
}
validateScale(scale) {
- if (!scale || _.isNaN(scale)) throw new Error('scale is ' + scale);
+ if (!scale || Number.isNaN(scale)) throw new Error('scale is ' + scale);
}
}
diff --git a/src/plugins/vis_type_xy/public/chart_splitter.tsx b/src/plugins/vis_type_xy/public/chart_splitter.tsx
new file mode 100644
index 0000000000000..bf63ac1896bd1
--- /dev/null
+++ b/src/plugins/vis_type_xy/public/chart_splitter.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+import { Accessor, AccessorFn, GroupBy, GroupBySort, SmallMultiples } from '@elastic/charts';
+
+interface ChartSplitterProps {
+ splitColumnAccessor?: Accessor | AccessorFn;
+ splitRowAccessor?: Accessor | AccessorFn;
+ sort?: GroupBySort;
+}
+
+const CHART_SPLITTER_ID = '__chart_splitter__';
+
+export const ChartSplitter = ({
+ splitColumnAccessor,
+ splitRowAccessor,
+ sort,
+}: ChartSplitterProps) =>
+ splitColumnAccessor || splitRowAccessor ? (
+ <>
+ {
+ const splitTypeAccessor = splitColumnAccessor || splitRowAccessor;
+ if (splitTypeAccessor) {
+ return typeof splitTypeAccessor === 'function'
+ ? splitTypeAccessor(datum)
+ : datum[splitTypeAccessor];
+ }
+ return spec.id;
+ }}
+ sort={sort || 'dataIndex'}
+ />
+
+ >
+ ) : null;
diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx
index 49b2ab483bc55..02c7157d32c27 100644
--- a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx
+++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx
@@ -16,19 +16,20 @@ import {
XYChartSeriesIdentifier,
} from '@elastic/charts';
-import { BUCKET_TYPES } from '../../../data/public';
-
import { Aspects } from '../types';
import './_detailed_tooltip.scss';
import { fillEmptyValue } from '../utils/get_series_name_fn';
-import { COMPLEX_SPLIT_ACCESSOR } from '../utils/accessors';
+import { COMPLEX_SPLIT_ACCESSOR, isRangeAggType } from '../utils/accessors';
interface TooltipData {
label: string;
value: string;
}
+// TODO: replace when exported from elastic/charts
+const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__';
+
const getTooltipData = (
aspects: Aspects,
header: TooltipValue | null,
@@ -37,10 +38,7 @@ const getTooltipData = (
const data: TooltipData[] = [];
if (header) {
- const xFormatter =
- aspects.x.aggType === BUCKET_TYPES.DATE_RANGE || aspects.x.aggType === BUCKET_TYPES.RANGE
- ? null
- : aspects.x.formatter;
+ const xFormatter = isRangeAggType(aspects.x.aggType) ? null : aspects.x.formatter;
data.push({
label: aspects.x.title,
value: xFormatter ? xFormatter(header.value) : `${header.value}`,
@@ -80,6 +78,28 @@ const getTooltipData = (
}
});
+ if (
+ aspects.splitColumn &&
+ valueSeries.smHorizontalAccessorValue !== undefined &&
+ valueSeries.smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE
+ ) {
+ data.push({
+ label: aspects.splitColumn.title,
+ value: `${valueSeries.smHorizontalAccessorValue}`,
+ });
+ }
+
+ if (
+ aspects.splitRow &&
+ valueSeries.smVerticalAccessorValue !== undefined &&
+ valueSeries.smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE
+ ) {
+ data.push({
+ label: aspects.splitRow.title,
+ value: `${valueSeries.smVerticalAccessorValue}`,
+ });
+ }
+
return data;
};
diff --git a/src/plugins/vis_type_xy/public/components/index.ts b/src/plugins/vis_type_xy/public/components/index.ts
index 260c08e0fc4a9..9b2559bafd18e 100644
--- a/src/plugins/vis_type_xy/public/components/index.ts
+++ b/src/plugins/vis_type_xy/public/components/index.ts
@@ -11,4 +11,3 @@ export { XYEndzones } from './xy_endzones';
export { XYCurrentTime } from './xy_current_time';
export { XYSettings } from './xy_settings';
export { XYThresholdLine } from './xy_threshold_line';
-export { SplitChartWarning } from './split_chart_warning';
diff --git a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx
deleted file mode 100644
index b708590e04479..0000000000000
--- a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * and the Server Side Public License, v 1; you may not use this file except in
- * compliance with, at your election, the Elastic License or the Server Side
- * Public License, v 1.
- */
-
-import React, { FC } from 'react';
-
-import { EuiLink, EuiCallOut } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import { getDocLinks } from '../services';
-
-export const SplitChartWarning: FC = () => {
- const advancedSettingsLink = getDocLinks().links.management.visualizationSettings;
-
- return (
-
-
-
-
- ),
- }}
- />
-
- );
-};
diff --git a/src/plugins/vis_type_xy/public/config/get_aspects.ts b/src/plugins/vis_type_xy/public/config/get_aspects.ts
index b8da4386806d4..c031d3fa1fb9b 100644
--- a/src/plugins/vis_type_xy/public/config/get_aspects.ts
+++ b/src/plugins/vis_type_xy/public/config/get_aspects.ts
@@ -29,7 +29,10 @@ export function getEmptyAspect(): Aspect {
},
};
}
-export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dimensions): Aspects {
+export function getAspects(
+ columns: DatatableColumn[],
+ { x, y, z, series, splitColumn, splitRow }: Dimensions
+): Aspects {
const seriesDimensions = Array.isArray(series) || series === undefined ? series : [series];
return {
@@ -37,6 +40,8 @@ export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dime
y: getAspectsFromDimension(columns, y) ?? [],
z: z && z?.length > 0 ? getAspectsFromDimension(columns, z[0]) : undefined,
series: getAspectsFromDimension(columns, seriesDimensions),
+ splitColumn: splitColumn?.length ? getAspectsFromDimension(columns, splitColumn[0]) : undefined,
+ splitRow: splitRow?.length ? getAspectsFromDimension(columns, splitRow[0]) : undefined,
};
}
diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts
index af3d840739f17..9d4660afa1634 100644
--- a/src/plugins/vis_type_xy/public/types/config.ts
+++ b/src/plugins/vis_type_xy/public/types/config.ts
@@ -43,6 +43,8 @@ export interface Aspects {
y: Aspect[];
z?: Aspect;
series?: Aspect[];
+ splitColumn?: Aspect;
+ splitRow?: Aspect;
}
export interface AxisGrid {
diff --git a/src/plugins/vis_type_xy/public/utils/accessors.tsx b/src/plugins/vis_type_xy/public/utils/accessors.tsx
index d1337251d36aa..e40248ae92e12 100644
--- a/src/plugins/vis_type_xy/public/utils/accessors.tsx
+++ b/src/plugins/vis_type_xy/public/utils/accessors.tsx
@@ -26,11 +26,15 @@ const getFieldName = (fieldName: string, index?: number) => {
return `${fieldName}${indexStr}`;
};
+export const isRangeAggType = (type: string | null) =>
+ type === BUCKET_TYPES.DATE_RANGE || type === BUCKET_TYPES.RANGE;
+
/**
* Returns accessor function for complex accessor types
* @param aspect
+ * @param isComplex - forces to be functional/complex accessor
*/
-export const getComplexAccessor = (fieldName: string) => (
+export const getComplexAccessor = (fieldName: string, isComplex: boolean = false) => (
aspect: Aspect,
index?: number
): Accessor | AccessorFn | undefined => {
@@ -38,12 +42,7 @@ export const getComplexAccessor = (fieldName: string) => (
return;
}
- if (
- !(
- (aspect.aggType === BUCKET_TYPES.DATE_RANGE || aspect.aggType === BUCKET_TYPES.RANGE) &&
- aspect.formatter
- )
- ) {
+ if (!((isComplex || isRangeAggType(aspect.aggType)) && aspect.formatter)) {
return aspect.accessor;
}
@@ -51,7 +50,7 @@ export const getComplexAccessor = (fieldName: string) => (
const accessor = aspect.accessor;
const fn: AccessorFn = (d) => {
const v = d[accessor];
- if (!v) {
+ if (v === undefined) {
return;
}
const f = formatter(v);
diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts
new file mode 100644
index 0000000000000..393a6ee06cf58
--- /dev/null
+++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts
@@ -0,0 +1,386 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { VisConfig } from '../types';
+
+export const getVisConfig = (): VisConfig => {
+ return {
+ markSizeRatio: 5.3999999999999995,
+ fittingFunction: 'linear',
+ detailedTooltip: true,
+ isTimeChart: true,
+ showCurrentTime: false,
+ showValueLabel: false,
+ enableHistogramMode: true,
+ tooltip: {
+ type: 'vertical',
+ },
+ aspects: {
+ x: {
+ accessor: 'col-0-2',
+ column: 0,
+ title: 'order_date per minute',
+ format: {
+ id: 'date',
+ params: {
+ pattern: 'HH:mm',
+ },
+ },
+ aggType: 'date_histogram',
+ aggId: '2',
+ params: {
+ date: true,
+ intervalESUnit: 'm',
+ intervalESValue: 1,
+ interval: 60000,
+ format: 'HH:mm',
+ },
+ },
+ y: [
+ {
+ accessor: 'col-1-3',
+ column: 1,
+ title: 'Average products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'avg',
+ aggId: '3',
+ params: {},
+ },
+ ],
+ },
+ xAxis: {
+ id: 'CategoryAxis-1',
+ position: 'bottom',
+ show: true,
+ style: {
+ axisTitle: {
+ visible: true,
+ },
+ tickLabel: {
+ visible: true,
+ rotation: 0,
+ },
+ },
+ groupId: 'CategoryAxis-1',
+ title: 'order_date per minute',
+ ticks: {
+ show: true,
+ showOverlappingLabels: false,
+ showDuplicates: false,
+ },
+ grid: {
+ show: false,
+ },
+ scale: {
+ type: 'time',
+ },
+ integersOnly: false,
+ },
+ yAxes: [
+ {
+ id: 'ValueAxis-1',
+ position: 'left',
+ show: true,
+ style: {
+ axisTitle: {
+ visible: true,
+ },
+ tickLabel: {
+ visible: true,
+ rotation: 0,
+ },
+ },
+ groupId: 'ValueAxis-1',
+ title: 'Percentiles of products.base_price',
+ ticks: {
+ show: true,
+ rotation: 0,
+ showOverlappingLabels: true,
+ showDuplicates: true,
+ },
+ grid: {
+ show: false,
+ },
+ scale: {
+ mode: 'normal',
+ type: 'linear',
+ },
+ domain: {},
+ integersOnly: false,
+ },
+ ],
+ legend: {
+ show: true,
+ position: 'right',
+ },
+ rotation: 0,
+ thresholdLine: {
+ color: '#E7664C',
+ show: false,
+ value: 10,
+ width: 1,
+ groupId: 'ValueAxis-1',
+ },
+ };
+};
+
+export const getVisConfigPercentiles = (): VisConfig => {
+ return {
+ markSizeRatio: 5.3999999999999995,
+ fittingFunction: 'linear',
+ detailedTooltip: true,
+ isTimeChart: true,
+ showCurrentTime: false,
+ showValueLabel: false,
+ enableHistogramMode: true,
+ tooltip: {
+ type: 'vertical',
+ },
+ aspects: {
+ x: {
+ accessor: 'col-0-2',
+ column: 0,
+ title: 'order_date per minute',
+ format: {
+ id: 'date',
+ params: {
+ pattern: 'HH:mm',
+ },
+ },
+ aggType: 'date_histogram',
+ aggId: '2',
+ params: {
+ date: true,
+ intervalESUnit: 'm',
+ intervalESValue: 1,
+ interval: 60000,
+ format: 'HH:mm',
+ },
+ },
+ y: [
+ {
+ accessor: 'col-1-3.1',
+ column: 1,
+ title: '1st percentile of products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'percentiles',
+ aggId: '3.1',
+ params: {},
+ },
+ {
+ accessor: 'col-2-3.5',
+ column: 2,
+ title: '5th percentile of products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'percentiles',
+ aggId: '3.5',
+ params: {},
+ },
+ {
+ accessor: 'col-3-3.25',
+ column: 3,
+ title: '25th percentile of products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'percentiles',
+ aggId: '3.25',
+ params: {},
+ },
+ {
+ accessor: 'col-4-3.50',
+ column: 4,
+ title: '50th percentile of products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'percentiles',
+ aggId: '3.50',
+ params: {},
+ },
+ {
+ accessor: 'col-5-3.75',
+ column: 5,
+ title: '75th percentile of products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'percentiles',
+ aggId: '3.75',
+ params: {},
+ },
+ {
+ accessor: 'col-6-3.95',
+ column: 6,
+ title: '95th percentile of products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'percentiles',
+ aggId: '3.95',
+ params: {},
+ },
+ {
+ accessor: 'col-7-3.99',
+ column: 7,
+ title: '99th percentile of products.base_price',
+ format: {
+ id: 'number',
+ },
+ aggType: 'percentiles',
+ aggId: '3.99',
+ params: {},
+ },
+ ],
+ },
+ xAxis: {
+ id: 'CategoryAxis-1',
+ position: 'bottom',
+ show: true,
+ style: {
+ axisTitle: {
+ visible: true,
+ },
+ tickLabel: {
+ visible: true,
+ rotation: 0,
+ },
+ },
+ groupId: 'CategoryAxis-1',
+ title: 'order_date per minute',
+ ticks: {
+ show: true,
+ showOverlappingLabels: false,
+ showDuplicates: false,
+ },
+ grid: {
+ show: false,
+ },
+ scale: {
+ type: 'time',
+ },
+ integersOnly: false,
+ },
+ yAxes: [
+ {
+ id: 'ValueAxis-1',
+ position: 'left',
+ show: true,
+ style: {
+ axisTitle: {
+ visible: true,
+ },
+ tickLabel: {
+ visible: true,
+ rotation: 0,
+ },
+ },
+ groupId: 'ValueAxis-1',
+ title: 'Percentiles of products.base_price',
+ ticks: {
+ show: true,
+ rotation: 0,
+ showOverlappingLabels: true,
+ showDuplicates: true,
+ },
+ grid: {
+ show: false,
+ },
+ scale: {
+ mode: 'normal',
+ type: 'linear',
+ },
+ domain: {},
+ integersOnly: false,
+ },
+ ],
+ legend: {
+ show: true,
+ position: 'right',
+ },
+ rotation: 0,
+ thresholdLine: {
+ color: '#E7664C',
+ show: false,
+ value: 10,
+ width: 1,
+ groupId: 'ValueAxis-1',
+ },
+ };
+};
+
+export const getPercentilesData = () => {
+ return [
+ {
+ 'col-0-2': 1610961900000,
+ 'col-1-3.1': 11.9921875,
+ 'col-2-3.5': 11.9921875,
+ 'col-3-3.25': 11.9921875,
+ 'col-4-3.50': 38.49609375,
+ 'col-5-3.75': 65,
+ 'col-6-3.95': 65,
+ 'col-7-3.99': 65,
+ },
+ {
+ 'col-0-2': 1610962980000,
+ 'col-1-3.1': 28.984375000000004,
+ 'col-2-3.5': 28.984375,
+ 'col-3-3.25': 28.984375,
+ 'col-4-3.50': 30.9921875,
+ 'col-5-3.75': 41.5,
+ 'col-6-3.95': 50,
+ 'col-7-3.99': 50,
+ },
+ {
+ 'col-0-2': 1610963280000,
+ 'col-1-3.1': 11.9921875,
+ 'col-2-3.5': 11.9921875,
+ 'col-3-3.25': 11.9921875,
+ 'col-4-3.50': 12.9921875,
+ 'col-5-3.75': 13.9921875,
+ 'col-6-3.95': 13.9921875,
+ 'col-7-3.99': 13.9921875,
+ },
+ {
+ 'col-0-2': 1610964180000,
+ 'col-1-3.1': 11.9921875,
+ 'col-2-3.5': 11.9921875,
+ 'col-3-3.25': 14.9921875,
+ 'col-4-3.50': 15.98828125,
+ 'col-5-3.75': 24.984375,
+ 'col-6-3.95': 85,
+ 'col-7-3.99': 85,
+ },
+ {
+ 'col-0-2': 1610964420000,
+ 'col-1-3.1': 11.9921875,
+ 'col-2-3.5': 11.9921875,
+ 'col-3-3.25': 11.9921875,
+ 'col-4-3.50': 23.99609375,
+ 'col-5-3.75': 42,
+ 'col-6-3.95': 42,
+ 'col-7-3.99': 42,
+ },
+ {
+ 'col-0-2': 1610964600000,
+ 'col-1-3.1': 10.9921875,
+ 'col-2-3.5': 10.992187500000002,
+ 'col-3-3.25': 10.9921875,
+ 'col-4-3.50': 12.4921875,
+ 'col-5-3.75': 13.9921875,
+ 'col-6-3.95': 13.9921875,
+ 'col-7-3.99': 13.9921875,
+ },
+ ];
+};
diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx
new file mode 100644
index 0000000000000..d76ea49a2f110
--- /dev/null
+++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx
@@ -0,0 +1,132 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { AreaSeries, BarSeries, CurveType } from '@elastic/charts';
+import { DatatableRow } from '../../../expressions/public';
+import { renderAllSeries } from './render_all_series';
+import {
+ getVisConfig,
+ getVisConfigPercentiles,
+ getPercentilesData,
+} from './render_all_series.test.mocks';
+import { SeriesParam, VisConfig } from '../types';
+
+const defaultSeriesParams = [
+ {
+ data: {
+ id: '3',
+ label: 'Label',
+ },
+ drawLinesBetweenPoints: true,
+ interpolate: 'linear',
+ lineWidth: 2,
+ mode: 'stacked',
+ show: true,
+ showCircles: true,
+ type: 'area',
+ valueAxis: 'ValueAxis-1',
+ },
+] as SeriesParam[];
+
+const defaultData = [
+ {
+ 'col-0-2': 1610960220000,
+ 'col-1-3': 26.984375,
+ },
+ {
+ 'col-0-2': 1610961300000,
+ 'col-1-3': 30.99609375,
+ },
+ {
+ 'col-0-2': 1610961900000,
+ 'col-1-3': 38.49609375,
+ },
+ {
+ 'col-0-2': 1610962980000,
+ 'col-1-3': 35.2421875,
+ },
+];
+
+describe('renderAllSeries', function () {
+ const getAllSeries = (visConfig: VisConfig, params: SeriesParam[], data: DatatableRow[]) => {
+ return renderAllSeries(
+ visConfig,
+ params,
+ data,
+ jest.fn(),
+ jest.fn(),
+ 'Europe/Athens',
+ 'col-0-2',
+ []
+ );
+ };
+
+ it('renders an area Series and not a bar series if type is area', () => {
+ const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData);
+ const wrapper = shallow(
{renderSeries}
);
+ expect(wrapper.find(AreaSeries).length).toBe(1);
+ expect(wrapper.find(BarSeries).length).toBe(0);
+ });
+
+ it('renders a bar Series in case of histogram', () => {
+ const barSeriesParams = [{ ...defaultSeriesParams[0], type: 'histogram' }];
+
+ const renderBarSeries = renderAllSeries(
+ getVisConfig(),
+ barSeriesParams as SeriesParam[],
+ defaultData,
+ jest.fn(),
+ jest.fn(),
+ 'Europe/Athens',
+ 'col-0-2',
+ []
+ );
+ const wrapper = shallow(
);
diff --git a/src/plugins/vis_type_xy/public/vis_renderer.tsx b/src/plugins/vis_type_xy/public/vis_renderer.tsx
index 612388939d26b..1a47742b3d004 100644
--- a/src/plugins/vis_type_xy/public/vis_renderer.tsx
+++ b/src/plugins/vis_type_xy/public/vis_renderer.tsx
@@ -16,7 +16,6 @@ import { VisualizationContainer } from '../../visualizations/public';
import type { PersistedState } from '../../visualizations/public';
import { XyVisType } from '../common';
-import { SplitChartWarning } from './components/split_chart_warning';
import { VisComponentType } from './vis_component';
import { RenderValue, visName } from './xy_vis_fn';
@@ -36,24 +35,20 @@ export const xyVisRenderer: ExpressionRenderDefinition = {
reuseDomNode: true,
render: async (domNode, { visData, visConfig, visType, syncColors }, handlers) => {
const showNoResult = shouldShowNoResultsMessage(visData, visType);
- const isSplitChart = Boolean(visConfig.dimensions.splitRow);
handlers.onDestroy(() => unmountComponentAtNode(domNode));
render(
- <>
- {isSplitChart && }
-
-
-
- >
+
+
+ ,
domNode
);
diff --git a/src/plugins/vis_type_xy/public/vis_types/area.tsx b/src/plugins/vis_type_xy/public/vis_types/area.ts
similarity index 94%
rename from src/plugins/vis_type_xy/public/vis_types/area.tsx
rename to src/plugins/vis_type_xy/public/vis_types/area.ts
index 50721c349d6e9..09007a01ca8bc 100644
--- a/src/plugins/vis_type_xy/public/vis_types/area.tsx
+++ b/src/plugins/vis_type_xy/public/vis_types/area.ts
@@ -6,8 +6,6 @@
* Public License, v 1.
*/
-import React from 'react';
-
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
@@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast';
import { ChartType } from '../../common';
import { getConfigCollections } from '../editor/collections';
import { getOptionTabs } from '../editor/common_config';
-import { SplitTooltip } from './split_tooltip';
export const getAreaVisTypeDefinition = (
showElasticChartsOptions = false
@@ -181,12 +178,6 @@ export const getAreaVisTypeDefinition = (
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
- // TODO: Remove when split chart aggs are supported
- // https://github.com/elastic/kibana/issues/82496
- ...(showElasticChartsOptions && {
- disabled: true,
- tooltip: ,
- }),
},
],
},
diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx b/src/plugins/vis_type_xy/public/vis_types/histogram.ts
similarity index 94%
rename from src/plugins/vis_type_xy/public/vis_types/histogram.tsx
rename to src/plugins/vis_type_xy/public/vis_types/histogram.ts
index 4fc8dbbb80e7b..daae5f5e48e61 100644
--- a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx
+++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts
@@ -6,8 +6,6 @@
* Public License, v 1.
*/
-import React from 'react';
-
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
@@ -30,7 +28,6 @@ import { ChartType } from '../../common';
import { getConfigCollections } from '../editor/collections';
import { getOptionTabs } from '../editor/common_config';
import { defaultCountLabel, LabelRotation } from '../../../charts/public';
-import { SplitTooltip } from './split_tooltip';
export const getHistogramVisTypeDefinition = (
showElasticChartsOptions = false
@@ -184,12 +181,6 @@ export const getHistogramVisTypeDefinition = (
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
- // TODO: Remove when split chart aggs are supported
- // https://github.com/elastic/kibana/issues/82496
- ...(showElasticChartsOptions && {
- disabled: true,
- tooltip: ,
- }),
},
],
},
diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts
similarity index 94%
rename from src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx
rename to src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts
index b53bb7bc9dd40..9e026fa0d7474 100644
--- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx
+++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts
@@ -6,8 +6,6 @@
* Public License, v 1.
*/
-import React from 'react';
-
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
@@ -30,7 +28,6 @@ import { ChartType } from '../../common';
import { getConfigCollections } from '../editor/collections';
import { getOptionTabs } from '../editor/common_config';
import { defaultCountLabel, LabelRotation } from '../../../charts/public';
-import { SplitTooltip } from './split_tooltip';
export const getHorizontalBarVisTypeDefinition = (
showElasticChartsOptions = false
@@ -183,12 +180,6 @@ export const getHorizontalBarVisTypeDefinition = (
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
- // TODO: Remove when split chart aggs are supported
- // https://github.com/elastic/kibana/issues/82496
- ...(showElasticChartsOptions && {
- disabled: true,
- tooltip: ,
- }),
},
],
},
diff --git a/src/plugins/vis_type_xy/public/vis_types/line.tsx b/src/plugins/vis_type_xy/public/vis_types/line.ts
similarity index 94%
rename from src/plugins/vis_type_xy/public/vis_types/line.tsx
rename to src/plugins/vis_type_xy/public/vis_types/line.ts
index e9b0533b957f5..3f3087207fa19 100644
--- a/src/plugins/vis_type_xy/public/vis_types/line.tsx
+++ b/src/plugins/vis_type_xy/public/vis_types/line.ts
@@ -6,8 +6,6 @@
* Public License, v 1.
*/
-import React from 'react';
-
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
@@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast';
import { ChartType } from '../../common';
import { getConfigCollections } from '../editor/collections';
import { getOptionTabs } from '../editor/common_config';
-import { SplitTooltip } from './split_tooltip';
export const getLineVisTypeDefinition = (
showElasticChartsOptions = false
@@ -175,12 +172,6 @@ export const getLineVisTypeDefinition = (
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
- // TODO: Remove when split chart aggs are supported
- // https://github.com/elastic/kibana/issues/82496
- ...(showElasticChartsOptions && {
- disabled: true,
- tooltip: ,
- }),
},
],
},
diff --git a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx b/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx
deleted file mode 100644
index ca22136599341..0000000000000
--- a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * and the Server Side Public License, v 1; you may not use this file except in
- * compliance with, at your election, the Elastic License or the Server Side
- * Public License, v 1.
- */
-
-import React from 'react';
-
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export function SplitTooltip() {
- return (
-
- );
-}
diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts
index bd7957164fd1a..fa3dddfeca02a 100644
--- a/src/plugins/vis_type_xy/server/plugin.ts
+++ b/src/plugins/vis_type_xy/server/plugin.ts
@@ -24,8 +24,7 @@ export const uiSettingsConfig: Record> = {
description: i18n.translate(
'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description',
{
- defaultMessage:
- 'Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation.',
+ defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.',
}
),
category: ['visualization'],
diff --git a/src/plugins/visualizations/common/index.ts b/src/plugins/visualizations/common/index.ts
new file mode 100644
index 0000000000000..d4133eb9b7163
--- /dev/null
+++ b/src/plugins/visualizations/common/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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+/** @public types */
+export * from './types';
diff --git a/src/plugins/visualizations/common/types.ts b/src/plugins/visualizations/common/types.ts
new file mode 100644
index 0000000000000..4881b82a0e8d3
--- /dev/null
+++ b/src/plugins/visualizations/common/types.ts
@@ -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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SavedObjectAttributes } from 'kibana/server';
+import { AggConfigOptions } from 'src/plugins/data/common';
+
+export interface VisParams {
+ [key: string]: any;
+}
+
+export interface SavedVisState {
+ title: string;
+ type: string;
+ params: TVisParams;
+ aggs: AggConfigOptions[];
+}
+
+export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes {
+ description: string;
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: string;
+ };
+ title: string;
+ version: number;
+ visState: string;
+ uiStateJSON: string;
+}
diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts
index d1976cc84acec..0bf8aa6e5c418 100644
--- a/src/plugins/visualizations/public/index.ts
+++ b/src/plugins/visualizations/public/index.ts
@@ -34,7 +34,7 @@ export type {
Schema,
ISchemas,
} from './vis_types';
-export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis';
+export { SerializedVis, SerializedVisData, VisData } from './vis';
export type VisualizeEmbeddableFactoryContract = PublicContract;
export type VisualizeEmbeddableContract = PublicContract;
export { VisualizeInput } from './embeddable';
@@ -46,12 +46,13 @@ export { PersistedState } from './persisted_state';
export {
VisualizationControllerConstructor,
VisualizationController,
- SavedVisState,
ISavedVis,
VisSavedObject,
VisResponseValue,
VisToExpressionAst,
+ VisParams,
} from './types';
export { ExprVisAPIEvents } from './expressions/vis';
export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry';
export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';
+export { SavedVisState } from '../common';
diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts
index f3643ad6adcbe..0d871b3b1dea4 100644
--- a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts
+++ b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts
@@ -6,7 +6,7 @@
* Public License, v 1.
*/
-import { SavedVisState } from '../types';
+import { SavedVisState } from '../../common';
declare function updateOldState(oldState: unknown): SavedVisState;
diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts
index c858306968ad8..a85a1d453a939 100644
--- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts
+++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts
@@ -7,7 +7,8 @@
*/
import { extractReferences, injectReferences } from './saved_visualization_references';
-import { VisSavedObject, SavedVisState } from '../types';
+import { VisSavedObject } from '../types';
+import { SavedVisState } from '../../common';
describe('extractReferences', () => {
test('extracts nothing if savedSearchId is empty', () => {
diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts
index 2e57cd00486f7..dc9ca49840561 100644
--- a/src/plugins/visualizations/public/types.ts
+++ b/src/plugins/visualizations/public/types.ts
@@ -7,15 +7,12 @@
*/
import { SavedObject } from '../../../plugins/saved_objects/public';
-import {
- AggConfigOptions,
- SearchSourceFields,
- TimefilterContract,
-} from '../../../plugins/data/public';
+import { SearchSourceFields, TimefilterContract } from '../../../plugins/data/public';
import { ExpressionAstExpression } from '../../expressions/public';
-import { SerializedVis, Vis, VisParams } from './vis';
+import { SerializedVis, Vis } from './vis';
import { ExprVis } from './expressions/vis';
+import { SavedVisState, VisParams } from '../common/types';
export { Vis, SerializedVis, VisParams };
@@ -30,13 +27,6 @@ export type VisualizationControllerConstructor = new (
vis: ExprVis
) => VisualizationController;
-export interface SavedVisState {
- title: string;
- type: string;
- params: VisParams;
- aggs: AggConfigOptions[];
-}
-
export interface ISavedVis {
id?: string;
title: string;
diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts
index 58bcdb9ea49c6..56a151fb82ed3 100644
--- a/src/plugins/visualizations/public/vis.ts
+++ b/src/plugins/visualizations/public/vis.ts
@@ -30,6 +30,7 @@ import {
AggConfigOptions,
SearchSourceFields,
} from '../../../plugins/data/public';
+import { VisParams } from '../common/types';
export interface SerializedVisData {
expression?: string;
@@ -56,10 +57,6 @@ export interface VisData {
savedSearchId?: string;
}
-export interface VisParams {
- [key: string]: any;
-}
-
const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => {
const searchSource = inputSearchSource.createCopy();
if (savedSearchId) {
diff --git a/test/examples/config.js b/test/examples/config.js
index a720899a637de..aab71cb305016 100644
--- a/test/examples/config.js
+++ b/test/examples/config.js
@@ -19,6 +19,7 @@ export default async function ({ readConfigFile }) {
require.resolve('./ui_actions'),
require.resolve('./state_sync'),
require.resolve('./routing'),
+ require.resolve('./expressions_explorer'),
],
services: {
...functionalConfig.get('services'),
diff --git a/test/examples/expressions_explorer/expressions.ts b/test/examples/expressions_explorer/expressions.ts
new file mode 100644
index 0000000000000..7261564e6db38
--- /dev/null
+++ b/test/examples/expressions_explorer/expressions.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+
+import { PluginFunctionalProviderContext } from 'test/plugin_functional/services';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService }: PluginFunctionalProviderContext) {
+ const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
+ const browser = getService('browser');
+
+ describe('', () => {
+ it('runs expression', async () => {
+ await retry.try(async () => {
+ const text = await testSubjects.getVisibleText('expressionResult');
+ expect(text).to.be(
+ '{\n "type": "error",\n "error": {\n "message": "Function markdown could not be found.",\n "name": "fn not found"\n }\n}'
+ );
+ });
+ });
+
+ it('renders expression', async () => {
+ await retry.try(async () => {
+ const text = await testSubjects.getVisibleText('expressionRender');
+ expect(text).to.be('Function markdown could not be found.');
+ });
+ });
+
+ it('emits an action and navigates', async () => {
+ await testSubjects.click('testExpressionButton');
+ await retry.try(async () => {
+ const text = await browser.getCurrentUrl();
+ expect(text).to.be('https://www.google.com/?gws_rd=ssl');
+ });
+ });
+ });
+}
diff --git a/test/examples/expressions_explorer/index.ts b/test/examples/expressions_explorer/index.ts
new file mode 100644
index 0000000000000..77d2a594c0f29
--- /dev/null
+++ b/test/examples/expressions_explorer/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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { PluginFunctionalProviderContext } from 'test/plugin_functional/services';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({
+ getService,
+ getPageObjects,
+ loadTestFile,
+}: PluginFunctionalProviderContext) {
+ const browser = getService('browser');
+ const PageObjects = getPageObjects(['common', 'header']);
+
+ describe('expressions explorer', function () {
+ before(async () => {
+ await browser.setWindowSize(1300, 900);
+ await PageObjects.common.navigateToApp('expressionsExplorer');
+ });
+
+ loadTestFile(require.resolve('./expressions'));
+ });
+}
diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts
index 07b5dfb8a769d..9d4623feef74a 100644
--- a/test/functional/apps/visualize/_inspector.ts
+++ b/test/functional/apps/visualize/_inspector.ts
@@ -6,12 +6,15 @@
* Public License, v 1.
*/
+import expect from '@kbn/expect';
+
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const inspector = getService('inspector');
const filterBar = getService('filterBar');
+ const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']);
describe('inspector', function describeIndexTests() {
@@ -23,6 +26,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.timePicker.setDefaultAbsoluteRange();
});
+ describe('advanced input JSON', () => {
+ it('should have "missing" property with value 10', async () => {
+ log.debug('Add Max Metric on memory field');
+ await PageObjects.visEditor.clickBucket('Y-axis', 'metrics');
+ await PageObjects.visEditor.selectAggregation('Max', 'metrics');
+ await PageObjects.visEditor.selectField('memory', 'metrics');
+
+ log.debug('Add value to advanced JSON input');
+ await PageObjects.visEditor.toggleAdvancedParams('2');
+ await testSubjects.setValue('codeEditorContainer', '{ "missing": 10 }');
+ await PageObjects.visEditor.clickGo();
+
+ await inspector.open();
+ await inspector.openInspectorRequestsView();
+ const requestTab = await inspector.getOpenRequestDetailRequestButton();
+ await requestTab.click();
+ const requestJSON = JSON.parse(await inspector.getCodeEditorValue());
+
+ expect(requestJSON.aggs['2'].max).property('missing', 10);
+ });
+
+ after(async () => {
+ await inspector.close();
+ await PageObjects.visEditor.removeDimension(2);
+ await PageObjects.visEditor.clickGo();
+ });
+ });
+
describe('inspector table', function indexPatternCreation() {
it('should update table header when columns change', async function () {
await inspector.open();
diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts
index aeb80a58c9655..3e74bf0b7c0ec 100644
--- a/test/functional/apps/visualize/_line_chart_split_chart.ts
+++ b/test/functional/apps/visualize/_line_chart_split_chart.ts
@@ -176,8 +176,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false);
await PageObjects.visEditor.clickGo();
const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers();
- const minLabel = 2;
- const maxLabel = 5000;
+ const minLabel = await PageObjects.visChart.getExpectedValue(2, 1);
+ const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000);
const numberOfLabels = 10;
expect(labels.length).to.be.greaterThan(numberOfLabels);
expect(labels[0]).to.eql(minLabel);
@@ -188,8 +188,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true);
await PageObjects.visEditor.clickGo();
const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers();
- const minLabel = 2;
- const maxLabel = 5000;
+ const minLabel = await PageObjects.visChart.getExpectedValue(2, 1);
+ const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000);
const numberOfLabels = 10;
expect(labels.length).to.be.greaterThan(numberOfLabels);
expect(labels[0]).to.eql(minLabel);
@@ -201,7 +201,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false);
await PageObjects.visEditor.clickGo();
const labels = await PageObjects.visChart.getYAxisLabels();
- const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000'];
+ const expectedLabels = await PageObjects.visChart.getExpectedValue(
+ ['0', '2,000', '4,000', '6,000', '8,000', '10,000'],
+ ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000']
+ );
expect(labels).to.eql(expectedLabels);
});
@@ -210,7 +213,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true);
await PageObjects.visEditor.clickGo();
const labels = await PageObjects.visChart.getYAxisLabels();
- const expectedLabels = ['2,000', '4,000', '6,000', '8,000'];
+ const expectedLabels = await PageObjects.visChart.getExpectedValue(
+ ['2,000', '4,000', '6,000', '8,000'],
+ ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000']
+ );
expect(labels).to.eql(expectedLabels);
});
@@ -220,7 +226,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visEditor.clickGo();
const labels = await PageObjects.visChart.getYAxisLabels();
log.debug(labels);
- const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000'];
+ const expectedLabels = await PageObjects.visChart.getExpectedValue(
+ ['0', '2,000', '4,000', '6,000', '8,000', '10,000'],
+ ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000']
+ );
expect(labels).to.eql(expectedLabels);
});
@@ -228,7 +237,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true);
await PageObjects.visEditor.clickGo();
const labels = await PageObjects.visChart.getYAxisLabels();
- const expectedLabels = ['2,000', '4,000', '6,000', '8,000'];
+ const expectedLabels = await PageObjects.visChart.getExpectedValue(
+ ['2,000', '4,000', '6,000', '8,000'],
+ ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000']
+ );
expect(labels).to.eql(expectedLabels);
});
});
diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts
index dddcd82f1d3f8..8dd2854419693 100644
--- a/test/functional/apps/visualize/index.ts
+++ b/test/functional/apps/visualize/index.ts
@@ -52,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
// Test replaced vislib chart types
loadTestFile(require.resolve('./_area_chart'));
loadTestFile(require.resolve('./_line_chart_split_series'));
+ loadTestFile(require.resolve('./_line_chart_split_chart'));
loadTestFile(require.resolve('./_point_series_options'));
loadTestFile(require.resolve('./_vertical_bar_chart'));
loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex'));
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 12c3ab12a6998..9472cbf400a6a 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -69,21 +69,26 @@ Table of Contents
- [`secrets`](#secrets-6)
- [`params`](#params-6)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice)
- - [`subActionParams (getFields)`](#subactionparams-getfields-1)
+ - [`subActionParams (getFields)`](#subactionparams-getfields)
- [Jira](#jira)
- [`config`](#config-7)
- [`secrets`](#secrets-7)
- [`params`](#params-7)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1)
+ - [`subActionParams (getIncident)`](#subactionparams-getincident)
- [`subActionParams (issueTypes)`](#subactionparams-issuetypes)
- - [`subActionParams (getFields)`](#subactionparams-getfields-2)
- - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
+ - [`subActionParams (fieldsByIssueType)`](#subactionparams-fieldsbyissuetype)
+ - [`subActionParams (issues)`](#subactionparams-issues)
+ - [`subActionParams (issue)`](#subactionparams-issue)
+ - [`subActionParams (getFields)`](#subactionparams-getfields-1)
- [IBM Resilient](#ibm-resilient)
- [`config`](#config-8)
- [`secrets`](#secrets-8)
- [`params`](#params-8)
- - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3)
- - [`subActionParams (getFields)`](#subactionparams-getfields-3)
+ - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
+ - [`subActionParams (getFields)`](#subactionparams-getfields-2)
+ - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes)
+ - [`subActionParams (severity)`](#subactionparams-severity)
- [Command Line Utility](#command-line-utility)
- [Developing New Action Types](#developing-new-action-types)
- [licensing](#licensing)
@@ -526,17 +531,17 @@ The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com
### `params`
-| Property | Description | Type |
-| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
-| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ |
-| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ |
-| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ |
-| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ |
-| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ |
-| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ |
-| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ |
-| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ |
-| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ |
+| Property | Description | Type |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
+| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ |
+| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ |
+| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ |
+| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ |
+| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ |
+| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ |
+| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ |
+| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ |
+| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ |
For more details see [PagerDuty v2 event parameters](https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2).
@@ -550,9 +555,9 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a
### `config`
-| Property | Description | Type |
-| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- |
-| apiUrl | ServiceNow instance URL. | string |
+| Property | Description | Type |
+| -------- | ------------------------ | ------ |
+| apiUrl | ServiceNow instance URL. | string |
### `secrets`
@@ -563,24 +568,28 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a
### `params`
-| Property | Description | Type |
-| --------------- | ------------------------------------------------------------------------------------ | ------ |
-| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string |
-| subActionParams | The parameters of the sub action | object |
+| Property | Description | Type |
+| --------------- | --------------------------------------------------------------------- | ------ |
+| subAction | The sub action to perform. It can be `getFields`, and `pushToService` | string |
+| subActionParams | The parameters of the sub action | object |
#### `subActionParams (pushToService)`
-| Property | Description | Type |
-| ------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------- |
-| savedObjectId | The id of the saved object. | string |
-| title | The title of the incident. | string _(optional)_ |
-| description | The description of the incident. | string _(optional)_ |
-| comment | A comment. | string _(optional)_ |
-| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
-| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
-| severity | The name of the severity in ServiceNow. | string _(optional)_ |
-| urgency | The name of the urgency in ServiceNow. | string _(optional)_ |
-| impact | The name of the impact in ServiceNow. | string _(optional)_ |
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The ServiceNow incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+The following table describes the properties of the `incident` object.
+
+| Property | Description | Type |
+| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- |
+| short_description | The title of the incident. | string |
+| description | The description of the incident. | string _(optional)_ |
+| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
+| severity | The name of the severity in ServiceNow. | string _(optional)_ |
+| urgency | The name of the urgency in ServiceNow. | string _(optional)_ |
+| impact | The name of the impact in ServiceNow. | string _(optional)_ |
#### `subActionParams (getFields)`
@@ -596,9 +605,9 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
### `config`
-| Property | Description | Type |
-| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
-| apiUrl | Jira instance URL. | string |
+| Property | Description | Type |
+| -------- | ------------------ | ------ |
+| apiUrl | Jira instance URL. | string |
### `secrets`
@@ -609,48 +618,71 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
### `params`
-| Property | Description | Type |
-| --------------- | ----------------------------------------------------------------------------------------------------------------------- | ------ |
-| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, `getIncident`, `issueTypes`, and `fieldsByIssueType` | string |
-| subActionParams | The parameters of the sub action | object |
+| Property | Description | Type |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
+| subAction | The sub action to perform. It can be `pushToService`, `getIncident`, `issueTypes`, `fieldsByIssueType`, `issues`, `issue`, and `getFields` | string |
+| subActionParams | The parameters of the sub action | object |
#### `subActionParams (pushToService)`
-| Property | Description | Type |
-| ------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- |
-| savedObjectId | The id of the saved object | string |
-| title | The title of the issue | string _(optional)_ |
-| description | The description of the issue | string _(optional)_ |
-| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
-| issueType | The id of the issue type in Jira. | string _(optional)_ |
-| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
-| labels | An array of labels. | string[] _(optional)_ |
-| parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ |
-| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The Jira incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
-#### `subActionParams (issueTypes)`
+The following table describes the properties of the `incident` object.
-No parameters for `issueTypes` sub-action. Provide an empty object `{}`.
+| Property | Description | Type |
+| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- |
+| summary | The title of the issue | string |
+| description | The description of the issue | string _(optional)_ |
+| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
+| issueType | The id of the issue type in Jira. | string _(optional)_ |
+| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
+| labels | An array of labels. | string[] _(optional)_ |
+| parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ |
-#### `subActionParams (getFields)`
+#### `subActionParams (getIncident)`
-No parameters for `getFields` sub-action. Provide an empty object `{}`.
+| Property | Description | Type |
+| ---------- | --------------------------- | ------ |
+| externalId | The id of the issue in Jira | string |
-#### `subActionParams (pushToService)`
+#### `subActionParams (issueTypes)`
+
+No parameters for `issueTypes` sub-action. Provide an empty object `{}`.
+
+#### `subActionParams (fieldsByIssueType)`
| Property | Description | Type |
| -------- | -------------------------------- | ------ |
| id | The id of the issue type in Jira | string |
+#### `subActionParams (issues)`
+
+| Property | Description | Type |
+| -------- | ----------------------- | ------ |
+| title | The title to search for | string |
+
+#### `subActionParams (issue)`
+
+| Property | Description | Type |
+| -------- | --------------------------- | ------ |
+| id | The id of the issue in Jira | string |
+
+#### `subActionParams (getFields)`
+
+No parameters for `getFields` sub-action. Provide an empty object `{}`.
+
## IBM Resilient
ID: `.resilient`
### `config`
-| Property | Description | Type |
-| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
-| apiUrl | IBM Resilient instance URL. | string |
+| Property | Description | Type |
+| -------- | --------------------------- | ------ |
+| apiUrl | IBM Resilient instance URL. | string |
### `secrets`
@@ -661,19 +693,24 @@ ID: `.resilient`
### `params`
-| Property | Description | Type |
-| --------------- | ------------------------------------------------------------------------------------ | ------ |
-| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string |
-| subActionParams | The parameters of the sub action | object |
+| Property | Description | Type |
+| --------------- | -------------------------------------------------------------------------------------------------- | ------ |
+| subAction | The sub action to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity` | string |
+| subActionParams | The parameters of the sub action | object |
#### `subActionParams (pushToService)`
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The IBM Resilient incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+The following table describes the properties of the `incident` object.
+
| Property | Description | Type |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- |
-| savedObjectId | The id of the saved object | string |
-| title | The title of the incident | string _(optional)_ |
+| name | The title of the incident | string _(optional)_ |
| description | The description of the incident | string _(optional)_ |
-| comments | The comments of the incident. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
| incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ |
| severityCode | IBM Resilient id of the severity code. | number _(optional)_ |
@@ -682,6 +719,14 @@ ID: `.resilient`
No parameters for `getFields` sub-action. Provide an empty object `{}`.
+#### `subActionParams (incidentTypes)`
+
+No parameters for `incidentTypes` sub-action. Provide an empty object `{}`.
+
+#### `subActionParams (severity)`
+
+No parameters for `severity` sub-action. Provide an empty object `{}`.
+
# Command Line Utility
The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command:
diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts
index ffc2a2ef21fe9..38922fa445a47 100644
--- a/x-pack/plugins/apm/common/ui_settings_keys.ts
+++ b/x-pack/plugins/apm/common/ui_settings_keys.ts
@@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const enableSignificantTerms = 'apm:enableSignificantTerms';
+export const enableCorrelations = 'apm:enableCorrelations';
export const enableServiceOverview = 'apm:enableServiceOverview';
diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx
index b2d88c4c3849b..438303110fbc4 100644
--- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx
+++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx
@@ -128,7 +128,7 @@ export function LatencyCorrelations() {
- View significant terms
+ View correlations
@@ -62,7 +62,7 @@ export function Correlations() {
>
-
Significant terms
+
Correlations
@@ -88,7 +88,7 @@ export function Correlations() {
iconType="alert"
>
- Significant terms is an experimental feature and in active
+ Correlations is an experimental feature and in active
development. Bugs and surprises are to be expected but let us
know your feedback so we can improve it.
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
index 392b42cba12e5..29d5750231762 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext } from 'react';
+import React, { useContext, useMemo } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -17,7 +17,7 @@ import { I18LABELS } from '../translations';
import { KeyUXMetrics } from './KeyUXMetrics';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useUxQuery } from '../hooks/useUxQuery';
-import { CoreVitals } from '../../../../../../observability/public';
+import { getCoreVitalsComponent } from '../../../../../../observability/public';
import { CsmSharedContext } from '../CsmSharedContext';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { getPercentileLabel } from './translations';
@@ -48,6 +48,18 @@ export function UXMetrics() {
sharedData: { totalPageViews },
} = useContext(CsmSharedContext);
+ const CoreVitals = useMemo(
+ () =>
+ getCoreVitalsComponent({
+ data,
+ totalPageViews,
+ loading: status !== 'success',
+ displayTrafficMetric: true,
+ }),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [status]
+ );
+
return (
@@ -67,12 +79,7 @@ export function UXMetrics() {
-
+ {CoreVitals}
diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts
index c86fb636b5a1a..e9bb747280fc7 100644
--- a/x-pack/plugins/apm/server/ui_settings.ts
+++ b/x-pack/plugins/apm/server/ui_settings.ts
@@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { UiSettingsParams } from '../../../../src/core/types';
import {
- enableSignificantTerms,
+ enableCorrelations,
enableServiceOverview,
} from '../common/ui_settings_keys';
@@ -16,17 +16,16 @@ import {
* uiSettings definitions for APM.
*/
export const uiSettings: Record> = {
- [enableSignificantTerms]: {
+ [enableCorrelations]: {
category: ['observability'],
name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', {
- defaultMessage: 'APM Significant terms (Platinum required)',
+ defaultMessage: 'APM correlations (Platinum required)',
}),
value: false,
description: i18n.translate(
'xpack.apm.enableCorrelationsExperimentDescription',
{
- defaultMessage:
- 'Enable the experimental Significant terms feature in APM',
+ defaultMessage: 'Enable the experimental correlations feature in APM',
}
),
schema: schema.boolean(),
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx
index b4fbba96e8dfb..341913a033c05 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx
@@ -26,9 +26,11 @@ export const debug: RendererFactory = () => ({
ReactDOM.render(renderDebug(), domNode, () => handlers.done());
- handlers.onResize(() => {
- ReactDOM.render(renderDebug(), domNode, () => handlers.done());
- });
+ if (handlers.onResize) {
+ handlers.onResize(() => {
+ ReactDOM.render(renderDebug(), domNode, () => handlers.done());
+ });
+ }
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
},
diff --git a/x-pack/plugins/case/README.md b/x-pack/plugins/case/README.md
index 30011148cd1e7..069441ab640ee 100644
--- a/x-pack/plugins/case/README.md
+++ b/x-pack/plugins/case/README.md
@@ -4,8 +4,7 @@
Elastic is developing a Case Management Workflow. Follow our progress:
-- [Case API Documentation](https://documenter.getpostman.com/view/172706/SW7c2SuF?version=latest)
-- [Github Meta](https://github.com/elastic/kibana/issues/50103)
+- [Case API Documentation](https://www.elastic.co/guide/en/security/master/cases-overview.html)
# Action types
@@ -42,27 +41,28 @@ This action type has no `secrets` properties.
| description | The case’s description. | string |
| tags | String array containing words and phrases that help categorize cases. | string[] |
| connector | Object containing the connector’s configuration. | [connector](#connector) |
+| settings | Object containing the case’s settings. | [settings](#settings) |
#### `subActionParams (update)`
-| Property | Description | Type |
-| ----------- | ---------------------------------------------------------- | ----------------------- |
-| id | The ID of the case being updated. | string |
-| tile | The updated case title. | string |
-| description | The updated case description. | string |
-| tags | The updated case tags. | string |
-| connector | Object containing the connector’s configuration. | [connector](#connector) |
-| status | The updated case status, which can be: `open` or `closed`. | string |
-| version | The current case version. | string |
+| Property | Description | Type |
+| ----------- | ------------------------------------------------------------------------- | ----------------------- |
+| id | The ID of the case being updated. | string |
+| tile | The updated case title. | string |
+| description | The updated case description. | string |
+| tags | The updated case tags. | string |
+| connector | Object containing the connector’s configuration. | [connector](#connector) |
+| status | The updated case status, which can be: `open`, `in-progress` or `closed`. | string |
+| settings | Object containing the case’s settings. | [settings](#settings) |
+| version | The current case version. | string |
#### `subActionParams (addComment)`
-| Property | Description | Type |
-| -------- | ----------------------------------------------------------------------- | ----------------- |
-| type | The type of the comment | `user` \| `alert` |
-| comment | The comment. Valid only when type is `user`. | string |
-| alertId | The alert ID. Valid only when the type is `alert` | string |
-| index | The index where the alert is saved. Valid only when the type is `alert` | string |
+| Property | Description | Type |
+| -------- | ------------------------ | ------ |
+| type | The type of the comment. | `user` |
+| comment | The comment. | string |
+
#### `connector`
| Property | Description | Type |
@@ -96,3 +96,9 @@ For IBM Resilient connectors:
| ------------ | ------------------------------- | -------- |
| issueTypes | The issue types of the issue. | string[] |
| severityCode | The severity code of the issue. | string |
+
+#### `settings`
+
+| Property | Description | Type |
+| ---------- | ------------------------------ | ------- |
+| syncAlerts | Turn on or off alert synching. | boolean |
\ No newline at end of file
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx
index 7326d2efb8565..8a4cf1d8566a0 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx
@@ -183,7 +183,7 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen
[http.basePath, state.start, state.end, logStreamQuery]
);
- const [logsPanelRef, { height: logPanelHeight }] = useMeasure();
+ const [logsPanelRef, { height: logPanelHeight }] = useMeasure();
const agentVersion = agent.local_metadata?.elastic?.agent?.version;
const isLogFeatureAvailable = useMemo(() => {
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
index 331b6bfa882da..94e81e296b5a9 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
@@ -115,7 +115,10 @@ async function deleteAssets(
try {
await Promise.all(deletePromises);
} catch (err) {
- logger.error(err);
+ // in the rollback case, partial installs are likely, so missing assets are not an error
+ if (!savedObjectsClient.errors.isNotFoundError(err)) {
+ logger.error(err);
+ }
}
}
diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts
index 1ce7b1d85c8e4..0dcdfeb7b3801 100644
--- a/x-pack/plugins/fleet/server/services/setup.ts
+++ b/x-pack/plugins/fleet/server/services/setup.ts
@@ -59,6 +59,7 @@ async function createSetupSideEffects(
ensureInstalledDefaultPackages(soClient, callCluster),
outputService.ensureDefaultOutput(soClient),
agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient),
+ updateFleetRoleIfExists(callCluster),
settingsService.getSettings(soClient).catch((e: any) => {
if (e.isBoom && e.output.statusCode === 404) {
const defaultSettings = createDefaultSettings();
@@ -126,15 +127,25 @@ async function createSetupSideEffects(
return { isIntialized: true };
}
-export async function setupFleet(
- soClient: SavedObjectsClientContract,
- esClient: ElasticsearchClient,
- callCluster: CallESAsCurrentUser,
- options?: { forceRecreate?: boolean }
-) {
- // Create fleet_enroll role
- // This should be done directly in ES at some point
- const res = await callCluster('transport.request', {
+async function updateFleetRoleIfExists(callCluster: CallESAsCurrentUser) {
+ try {
+ await callCluster('transport.request', {
+ method: 'GET',
+ path: `/_security/role/${FLEET_ENROLL_ROLE}`,
+ });
+ } catch (e) {
+ if (e.status === 404) {
+ return;
+ }
+
+ throw e;
+ }
+
+ return putFleetRole(callCluster);
+}
+
+async function putFleetRole(callCluster: CallESAsCurrentUser) {
+ return callCluster('transport.request', {
method: 'PUT',
path: `/_security/role/${FLEET_ENROLL_ROLE}`,
body: {
@@ -156,6 +167,18 @@ export async function setupFleet(
],
},
});
+}
+
+export async function setupFleet(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ callCluster: CallESAsCurrentUser,
+ options?: { forceRecreate?: boolean }
+) {
+ // Create fleet_enroll role
+ // This should be done directly in ES at some point
+ const res = await putFleetRole(callCluster);
+
// If the role is already created skip the rest unless you have forceRecreate set to true
if (options?.forceRecreate !== true && res.role.created === false) {
return;
diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts
index 1cc69ac2239ab..7c32671be93c4 100644
--- a/x-pack/plugins/ml/public/plugin.ts
+++ b/x-pack/plugins/ml/public/plugin.ts
@@ -70,7 +70,7 @@ export interface MlSetupDependencies {
export type MlCoreSetup = CoreSetup;
export class MlPlugin implements Plugin {
- private appUpdater = new BehaviorSubject(() => ({}));
+ private appUpdater$ = new BehaviorSubject(() => ({}));
private urlGenerator: undefined | UrlGeneratorContract;
constructor(private initializerContext: PluginInitializerContext) {}
@@ -85,7 +85,7 @@ export class MlPlugin implements Plugin {
euiIconType: PLUGIN_ICON_SOLUTION,
appRoute: '/app/ml',
category: DEFAULT_APP_CATEGORIES.kibana,
- updater$: this.appUpdater,
+ updater$: this.appUpdater$,
mount: async (params: AppMountParameters) => {
const [coreStart, pluginsStart] = await core.getStartServices();
const kibanaVersion = this.initializerContext.env.packageInfo.version;
@@ -133,23 +133,34 @@ export class MlPlugin implements Plugin {
});
} else {
// if ml is disabled in elasticsearch, disable ML in kibana
- this.appUpdater.next(() => ({
+ this.appUpdater$.next(() => ({
status: AppStatus.inaccessible,
}));
}
// register various ML plugin features which require a full license
- const { registerEmbeddables, registerManagementSection, registerMlUiActions } = await import(
- './register_helper'
- );
-
- if (isMlEnabled(license) && isFullLicense(license)) {
- const canManageMLJobs = capabilities.management?.insightsAndAlerting?.jobsListLink ?? false;
- if (canManageMLJobs && pluginsSetup.management !== undefined) {
- registerManagementSection(pluginsSetup.management, core).enable();
+ // note including registerFeature in register_helper would cause the page bundle size to increase significantly
+ const {
+ registerEmbeddables,
+ registerManagementSection,
+ registerMlUiActions,
+ registerSearchLinks,
+ } = await import('./register_helper');
+
+ const mlEnabled = isMlEnabled(license);
+ const fullLicense = isFullLicense(license);
+ if (mlEnabled) {
+ registerSearchLinks(this.appUpdater$, fullLicense);
+
+ if (fullLicense) {
+ const canManageMLJobs =
+ capabilities.management?.insightsAndAlerting?.jobsListLink ?? false;
+ if (canManageMLJobs && pluginsSetup.management !== undefined) {
+ registerManagementSection(pluginsSetup.management, core).enable();
+ }
+ registerEmbeddables(pluginsSetup.embeddable, core);
+ registerMlUiActions(pluginsSetup.uiActions, core);
}
- registerEmbeddables(pluginsSetup.embeddable, core);
- registerMlUiActions(pluginsSetup.uiActions, core);
}
});
diff --git a/x-pack/plugins/ml/public/register_helper/index.ts b/x-pack/plugins/ml/public/register_helper/index.ts
new file mode 100644
index 0000000000000..8e62b6562520a
--- /dev/null
+++ b/x-pack/plugins/ml/public/register_helper/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { registerEmbeddables } from '../embeddables';
+export { registerManagementSection } from '../application/management';
+export { registerMlUiActions } from '../ui_actions';
+export { registerSearchLinks } from './register_search_links';
diff --git a/x-pack/plugins/ml/public/register_helper.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/index.ts
similarity index 51%
rename from x-pack/plugins/ml/public/register_helper.ts
rename to x-pack/plugins/ml/public/register_helper/register_search_links/index.ts
index 50ec53a10ece9..e1912c7ebabeb 100644
--- a/x-pack/plugins/ml/public/register_helper.ts
+++ b/x-pack/plugins/ml/public/register_helper/register_search_links/index.ts
@@ -4,7 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { registerEmbeddables } from './embeddables';
-export { registerFeature } from './register_feature';
-export { registerManagementSection } from './application/management';
-export { registerMlUiActions } from './ui_actions';
+export { registerSearchLinks } from './register_search_links';
diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts
new file mode 100644
index 0000000000000..2df7e8140698a
--- /dev/null
+++ b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { BehaviorSubject } from 'rxjs';
+
+import { AppUpdater } from 'src/core/public';
+import { getSearchDeepLinks } from './search_deep_links';
+
+export function registerSearchLinks(
+ appUpdater: BehaviorSubject,
+ isFullLicense: boolean
+) {
+ appUpdater.next(() => ({
+ meta: {
+ keywords: [
+ i18n.translate('xpack.ml.keyword.ml', {
+ defaultMessage: 'ML',
+ }),
+ ],
+ searchDeepLinks: getSearchDeepLinks(isFullLicense),
+ },
+ }));
+}
diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts
new file mode 100644
index 0000000000000..7108fb7af5670
--- /dev/null
+++ b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+import type { AppSearchDeepLink } from 'src/core/public';
+import { ML_PAGES } from '../../../common/constants/ml_url_generator';
+
+const OVERVIEW_LINK_SEARCH_DEEP_LINK: AppSearchDeepLink = {
+ id: 'mlOverviewSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.overview', {
+ defaultMessage: 'Overview',
+ }),
+ path: `/${ML_PAGES.OVERVIEW}`,
+};
+
+const ANOMALY_DETECTION_SEARCH_DEEP_LINK: AppSearchDeepLink = {
+ id: 'mlAnomalyDetectionSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.anomalyDetection', {
+ defaultMessage: 'Anomaly Detection',
+ }),
+ path: `/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`,
+};
+
+const DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK: AppSearchDeepLink = {
+ id: 'mlDataFrameAnalyticsSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.dataFrameAnalytics', {
+ defaultMessage: 'Data Frame Analytics',
+ }),
+ path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`,
+ searchDeepLinks: [
+ {
+ id: 'mlTrainedModelsSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.trainedModels', {
+ defaultMessage: 'Trained Models',
+ }),
+ path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE}`,
+ },
+ ],
+};
+
+const DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = {
+ id: 'mlDataVisualizerSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.dataVisualizer', {
+ defaultMessage: 'Data Visualizer',
+ }),
+ path: `/${ML_PAGES.DATA_VISUALIZER}`,
+};
+
+const FILE_UPLOAD_SEARCH_DEEP_LINK: AppSearchDeepLink = {
+ id: 'mlFileUploadSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.fileUpload', {
+ defaultMessage: 'File Upload',
+ }),
+ path: `/${ML_PAGES.DATA_VISUALIZER_FILE}`,
+};
+
+const INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = {
+ id: 'mlIndexDataVisualizerSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.indexDataVisualizer', {
+ defaultMessage: 'Index Data Visualizer',
+ }),
+ path: `/${ML_PAGES.DATA_VISUALIZER_INDEX_SELECT}`,
+};
+
+const SETTINGS_SEARCH_DEEP_LINK: AppSearchDeepLink = {
+ id: 'mlSettingsSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.settings', {
+ defaultMessage: 'Settings',
+ }),
+ path: `/${ML_PAGES.SETTINGS}`,
+ searchDeepLinks: [
+ {
+ id: 'mlCalendarSettingsSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.calendarSettings', {
+ defaultMessage: 'Calendars',
+ }),
+ path: `/${ML_PAGES.CALENDARS_MANAGE}`,
+ },
+ {
+ id: 'mlFilterListsSettingsSearchDeepLink',
+ title: i18n.translate('xpack.ml.searchDeepLink.filterListsSettings', {
+ defaultMessage: 'Filter Lists',
+ }),
+ path: `/${ML_PAGES.SETTINGS}`, // Link to settings page as read only users cannot view filter lists.
+ },
+ ],
+};
+
+export function getSearchDeepLinks(isFullLicense: boolean) {
+ const deepLinks: AppSearchDeepLink[] = [
+ DATA_VISUALIZER_SEARCH_DEEP_LINK,
+ FILE_UPLOAD_SEARCH_DEEP_LINK,
+ INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK,
+ ];
+
+ if (isFullLicense === true) {
+ deepLinks.push(
+ OVERVIEW_LINK_SEARCH_DEEP_LINK,
+ ANOMALY_DETECTION_SEARCH_DEEP_LINK,
+ DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK,
+ SETTINGS_SEARCH_DEEP_LINK
+ );
+ }
+
+ return deepLinks;
+}
diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts
index 3719f4d97779c..42f6eb1f8f9c8 100644
--- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts
+++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts
@@ -5,7 +5,12 @@
*/
import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
-import { Job } from '../../../common/types/anomaly_detection_jobs';
+import {
+ Job,
+ JobStats,
+ Datafeed,
+ DatafeedStats,
+} from '../../../common/types/anomaly_detection_jobs';
import { GetGuards } from '../shared_services';
export interface AnomalyDetectorsProvider {
@@ -14,6 +19,9 @@ export interface AnomalyDetectorsProvider {
savedObjectsClient: SavedObjectsClientContract
): {
jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>;
+ jobStats(jobId?: string): Promise<{ count: number; jobs: JobStats[] }>;
+ datafeeds(datafeedId?: string): Promise<{ count: number; datafeeds: Datafeed[] }>;
+ datafeedStats(datafeedId?: string): Promise<{ count: number; datafeeds: DatafeedStats[] }>;
};
}
@@ -36,6 +44,42 @@ export function getAnomalyDetectorsProvider(getGuards: GetGuards): AnomalyDetect
return body;
});
},
+ async jobStats(jobId?: string) {
+ return await getGuards(request, savedObjectsClient)
+ .isFullLicense()
+ .hasMlCapabilities(['canGetJobs'])
+ .ok(async ({ mlClient }) => {
+ const { body } = await mlClient.getJobStats<{
+ count: number;
+ jobs: JobStats[];
+ }>(jobId !== undefined ? { job_id: jobId } : undefined);
+ return body;
+ });
+ },
+ async datafeeds(datafeedId?: string) {
+ return await getGuards(request, savedObjectsClient)
+ .isFullLicense()
+ .hasMlCapabilities(['canGetDatafeeds'])
+ .ok(async ({ mlClient }) => {
+ const { body } = await mlClient.getDatafeeds<{
+ count: number;
+ datafeeds: Datafeed[];
+ }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined);
+ return body;
+ });
+ },
+ async datafeedStats(datafeedId?: string) {
+ return await getGuards(request, savedObjectsClient)
+ .isFullLicense()
+ .hasMlCapabilities(['canGetDatafeeds'])
+ .ok(async ({ mlClient }) => {
+ const { body } = await mlClient.getDatafeedStats<{
+ count: number;
+ datafeeds: DatafeedStats[];
+ }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined);
+ return body;
+ });
+ },
};
},
};
diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx
index b195bb52e7ed2..097871fe020e5 100644
--- a/x-pack/plugins/observability/public/components/app/header/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/header/index.tsx
@@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n';
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { usePluginContext } from '../../../hooks/use_plugin_context';
-import { HeaderMenuPortal } from '../../shared/header_menu_portal';
+import HeaderMenuPortal from '../../shared/header_menu_portal';
const Container = styled.div<{ color: string }>`
background: ${(props) => props.color};
diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx
index 43f1072d06fc2..7074a895d058b 100644
--- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx
@@ -12,7 +12,7 @@ import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { UXHasDataResponse } from '../../../../typings';
-import { CoreVitals } from '../../../shared/core_web_vitals';
+import CoreVitals from '../../../shared/core_web_vitals';
interface Props {
bucketSize: string;
diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx
index f573c8cfc1f97..7d40ce089cec4 100644
--- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx
@@ -16,6 +16,7 @@ import {
import { CoreVitalItem } from './core_vital_item';
import { WebCoreVitalsTitle } from './web_core_vitals_title';
import { ServiceName } from './service_name';
+import { CoreVitalProps } from '../types';
export interface UXMetrics {
cls: number | null;
@@ -29,7 +30,7 @@ export interface UXMetrics {
clsRanks: number[];
}
-export function formatToSec(value?: number | string, fromUnit = 'MicroSec'): string {
+function formatToSec(value?: number | string, fromUnit = 'MicroSec'): string {
const valueInMs = Number(value ?? 0) / (fromUnit === 'MicroSec' ? 1000 : 1);
if (valueInMs < 1000) {
@@ -51,23 +52,15 @@ const CoreVitalsThresholds = {
CLS: { good: '0.1', bad: '0.25' },
};
-interface Props {
- loading: boolean;
- data?: UXMetrics | null;
- displayServiceName?: boolean;
- serviceName?: string;
- totalPageViews?: number;
- displayTrafficMetric?: boolean;
-}
-
-export function CoreVitals({
+// eslint-disable-next-line import/no-default-export
+export default function CoreVitals({
data,
loading,
displayServiceName,
serviceName,
totalPageViews,
displayTrafficMetric = false,
-}: Props) {
+}: CoreVitalProps) {
const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks, coreVitalPages } = data || {};
return (
diff --git a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx
index ca03eb6ddb45a..e209e830d0f37 100644
--- a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx
+++ b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx
@@ -4,17 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { ReactNode, useEffect, useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
-import { AppMountParameters } from '../../../../../../src/core/public';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
+import { HeaderMenuPortalProps } from './types';
-interface HeaderMenuPortalProps {
- children: ReactNode;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
-}
-
-export function HeaderMenuPortal({ children, setHeaderActionMenu }: HeaderMenuPortalProps) {
+// eslint-disable-next-line import/no-default-export
+export default function HeaderMenuPortal({ children, setHeaderActionMenu }: HeaderMenuPortalProps) {
const portalNode = useMemo(() => createPortalNode(), []);
useEffect(() => {
diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx
new file mode 100644
index 0000000000000..6e3835129beb2
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/index.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { lazy, Suspense } from 'react';
+import { CoreVitalProps, HeaderMenuPortalProps } from './types';
+
+export function getCoreVitalsComponent(props: CoreVitalProps) {
+ const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index'));
+ return (
+
+
+
+ );
+}
+
+export function HeaderMenuPortal(props: HeaderMenuPortalProps) {
+ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal'));
+ return (
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/types.ts b/x-pack/plugins/observability/public/components/shared/types.ts
new file mode 100644
index 0000000000000..9039f444f550f
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ReactNode } from 'react';
+import { AppMountParameters } from '../../../../../../src/core/public';
+import { UXMetrics } from './core_web_vitals';
+
+export interface HeaderMenuPortalProps {
+ children: ReactNode;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
+}
+
+export interface CoreVitalProps {
+ loading: boolean;
+ data?: UXMetrics | null;
+ displayServiceName?: boolean;
+ serviceName?: string;
+ totalPageViews?: number;
+ displayTrafficMetric?: boolean;
+}
diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts
index 22cc5faf23967..c052541956c13 100644
--- a/x-pack/plugins/observability/public/index.ts
+++ b/x-pack/plugins/observability/public/index.ts
@@ -6,8 +6,7 @@
import { PluginInitializerContext, PluginInitializer } from 'kibana/public';
import { Plugin, ObservabilityPluginSetup, ObservabilityPluginStart } from './plugin';
-export { HeaderMenuPortal } from './components/shared/header_menu_portal';
-export { ObservabilityPluginSetup, ObservabilityPluginStart };
+export type { ObservabilityPluginSetup, ObservabilityPluginStart };
export const plugin: PluginInitializer = (
context: PluginInitializerContext
@@ -17,7 +16,8 @@ export const plugin: PluginInitializer {
);
});
- it('returns an error message if the name contains invalid characters', () => {
- expect(validateTagName('t^ag+name&')).toMatchInlineSnapshot(
- `"Tag name can only include a-z, 0-9, _, -,:."`
- );
+ it('does not return an error message if the name contains special characters', () => {
+ expect(validateTagName('t^ag+name&')).toBeUndefined();
});
});
diff --git a/x-pack/plugins/saved_objects_tagging/common/validation.ts b/x-pack/plugins/saved_objects_tagging/common/validation.ts
index 12149d7bdbe79..5cb9e068516fe 100644
--- a/x-pack/plugins/saved_objects_tagging/common/validation.ts
+++ b/x-pack/plugins/saved_objects_tagging/common/validation.ts
@@ -12,7 +12,6 @@ export const tagNameMaxLength = 50;
export const tagDescriptionMaxLength = 100;
const hexColorRegexp = /^#[0-9A-F]{6}$/i;
-const nameValidCharsRegexp = /^[0-9A-Z:\-_\s]+$/i;
export interface TagValidation {
valid: boolean;
@@ -49,11 +48,6 @@ export const validateTagName = (name: string): string | undefined => {
},
});
}
- if (!nameValidCharsRegexp.test(name)) {
- return i18n.translate('xpack.savedObjectsTagging.validation.name.errorInvalidCharacters', {
- defaultMessage: 'Tag name can only include a-z, 0-9, _, -,:.',
- });
- }
};
export const validateTagDescription = (description: string): string | undefined => {
diff --git a/x-pack/plugins/searchprofiler/server/routes/profile.ts b/x-pack/plugins/searchprofiler/server/routes/profile.ts
index 914c688a080f8..87f2ec1df1c92 100644
--- a/x-pack/plugins/searchprofiler/server/routes/profile.ts
+++ b/x-pack/plugins/searchprofiler/server/routes/profile.ts
@@ -27,10 +27,6 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) =
});
}
- const {
- core: { elasticsearch },
- } = ctx;
-
const {
body: { query, index },
} = request;
@@ -46,21 +42,25 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) =
body: JSON.stringify(parsed, null, 2),
};
try {
- const resp = await elasticsearch.legacy.client.callAsCurrentUser('search', body);
+ const client = ctx.core.elasticsearch.client.asCurrentUser;
+ const resp = await client.search(body);
+
return response.ok({
body: {
ok: true,
- resp,
+ resp: resp.body,
},
});
} catch (err) {
log.error(err);
+ const { statusCode, body: errorBody } = err;
+
return response.customError({
- statusCode: err.status || 500,
- body: err.body
+ statusCode: statusCode || 500,
+ body: errorBody
? {
- message: err.message,
- attributes: err.body,
+ message: errorBody.error?.reason,
+ attributes: errorBody,
}
: err,
});
diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts
index f53b5ca6d56ca..c235c296bcbae 100644
--- a/x-pack/plugins/security/common/constants.ts
+++ b/x-pack/plugins/security/common/constants.ts
@@ -22,3 +22,16 @@ export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint';
export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider';
export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg';
export const NEXT_URL_QUERY_STRING_PARAMETER = 'next';
+
+/**
+ * Matches valid usernames and role names.
+ *
+ * - Must contain only letters, numbers, spaces, punctuation and printable symbols.
+ * - Must not contain leading or trailing spaces.
+ */
+export const NAME_REGEX = /^(?! )[a-zA-Z0-9 !"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~]+(?) {
* @param {role} the Role as returned by roles API
*/
export function isRoleDeprecated(role: Partial) {
- return role.metadata?._deprecated ?? false;
+ return (role.metadata?._deprecated as boolean) ?? false;
+}
+
+/**
+ * Returns whether given role is a system role or not.
+ *
+ * @param {role} the Role as returned by roles API
+ */
+export function isRoleSystem(role: Partial) {
+ return (isRoleReserved(role) && role.name?.endsWith('_system')) ?? false;
+}
+
+/**
+ * Returns whether given role is an admin role or not.
+ *
+ * @param {role} the Role as returned by roles API
+ */
+export function isRoleAdmin(role: Partial) {
+ return (
+ (isRoleReserved(role) && (role.name?.endsWith('_admin') || role.name === 'superuser')) ?? false
+ );
}
/**
diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx
new file mode 100644
index 0000000000000..7246e37b33da9
--- /dev/null
+++ b/x-pack/plugins/security/public/components/breadcrumb.tsx
@@ -0,0 +1,137 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { createContext, useEffect, useRef, useContext, FunctionComponent } from 'react';
+import { EuiBreadcrumb } from '@elastic/eui';
+import { useKibana } from '../../../../../src/plugins/kibana_react/public';
+
+interface BreadcrumbsContext {
+ parents: BreadcrumbProps[];
+ onMount(breadcrumbs: BreadcrumbProps[]): void;
+ onUnmount(breadcrumbs: BreadcrumbProps[]): void;
+}
+
+const BreadcrumbsContext = createContext(undefined);
+
+export interface BreadcrumbProps extends EuiBreadcrumb {
+ text: string;
+}
+
+/**
+ * Component that automatically sets breadcrumbs and document title based on the render tree.
+ *
+ * @example
+ * // Breadcrumbs will be set to: "Users > Create"
+ * // Document title will be set to: "Create - Users"
+ *
+ * ```typescript
+ *
+ *
+ * {showForm && (
+ *
+ *
+ *
+ * )}
+ *
+ * ```
+ */
+export const Breadcrumb: FunctionComponent = ({ children, ...breadcrumb }) => {
+ const context = useContext(BreadcrumbsContext);
+ const component = {children};
+
+ if (context) {
+ return component;
+ }
+
+ return {component};
+};
+
+export interface BreadcrumbsProviderProps {
+ onChange?: BreadcrumbsChangeHandler;
+}
+
+export type BreadcrumbsChangeHandler = (breadcrumbs: BreadcrumbProps[]) => void;
+
+/**
+ * Component that can be used to define any side effects that should occur when breadcrumbs change.
+ *
+ * By default the breadcrumbs in application chrome are set and the document title is updated.
+ *
+ * @example
+ * ```typescript
+ * setBreadcrumbs(breadcrumbs)}>
+ *
+ *
+ * ```
+ */
+export const BreadcrumbsProvider: FunctionComponent = ({
+ children,
+ onChange,
+}) => {
+ const { services } = useKibana();
+ const breadcrumbsRef = useRef([]);
+
+ const handleChange = (breadcrumbs: BreadcrumbProps[]) => {
+ if (onChange) {
+ onChange(breadcrumbs);
+ } else if (services.chrome) {
+ services.chrome.setBreadcrumbs(breadcrumbs);
+ services.chrome.docTitle.change(getDocTitle(breadcrumbs));
+ }
+ };
+
+ return (
+ {
+ if (breadcrumbs.length > breadcrumbsRef.current.length) {
+ breadcrumbsRef.current = breadcrumbs;
+ handleChange(breadcrumbs);
+ }
+ },
+ onUnmount: (breadcrumbs) => {
+ if (breadcrumbs.length < breadcrumbsRef.current.length) {
+ breadcrumbsRef.current = breadcrumbs;
+ handleChange(breadcrumbs);
+ }
+ },
+ }}
+ >
+ {children}
+
+ );
+};
+
+export interface InnerBreadcrumbProps {
+ breadcrumb: BreadcrumbProps;
+}
+
+export const InnerBreadcrumb: FunctionComponent = ({
+ breadcrumb,
+ children,
+}) => {
+ const { parents, onMount, onUnmount } = useContext(BreadcrumbsContext)!;
+ const nextParents = [...parents, breadcrumb];
+
+ useEffect(() => {
+ onMount(nextParents);
+ return () => onUnmount(parents);
+ }, [breadcrumb.text, breadcrumb.href]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return (
+
+ {children}
+
+ );
+};
+
+export function getDocTitle(breadcrumbs: BreadcrumbProps[], maxBreadcrumbs = 2) {
+ return breadcrumbs
+ .slice(0, maxBreadcrumbs)
+ .reverse()
+ .map(({ text }) => text);
+}
diff --git a/x-pack/plugins/security/public/components/confirm_modal.tsx b/x-pack/plugins/security/public/components/confirm_modal.tsx
new file mode 100644
index 0000000000000..8dfbf9e3f0649
--- /dev/null
+++ b/x-pack/plugins/security/public/components/confirm_modal.tsx
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import {
+ EuiButton,
+ EuiButtonProps,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalProps,
+ EuiOverlayMask,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export interface ConfirmModalProps extends Omit {
+ confirmButtonText: string;
+ confirmButtonColor?: EuiButtonProps['color'];
+ isLoading?: EuiButtonProps['isLoading'];
+ isDisabled?: EuiButtonProps['isDisabled'];
+ onCancel(): void;
+ onConfirm(): void;
+ ownFocus?: boolean;
+}
+
+/**
+ * Component that renders a confirmation modal similar to `EuiConfirmModal`, except that
+ * it adds `isLoading` prop, which renders a loading spinner and disables action buttons,
+ * and `ownFocus` prop to render overlay mask.
+ */
+export const ConfirmModal: FunctionComponent = ({
+ children,
+ confirmButtonColor: buttonColor,
+ confirmButtonText,
+ isLoading,
+ isDisabled,
+ onCancel,
+ onConfirm,
+ ownFocus = true,
+ title,
+ ...rest
+}) => {
+ const modal = (
+
+
+ {title}
+
+ {children}
+
+
+
+
+
+
+
+
+
+ {confirmButtonText}
+
+
+
+
+
+ );
+
+ return ownFocus ? (
+ {modal}
+ ) : (
+ modal
+ );
+};
diff --git a/x-pack/plugins/security/public/components/doc_link.tsx b/x-pack/plugins/security/public/components/doc_link.tsx
new file mode 100644
index 0000000000000..50a93b8ee5090
--- /dev/null
+++ b/x-pack/plugins/security/public/components/doc_link.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, FunctionComponent } from 'react';
+import { EuiLink } from '@elastic/eui';
+import { useKibana } from '../../../../../src/plugins/kibana_react/public';
+import { CoreStart } from '../../../../../src/core/public';
+
+export type DocLinks = CoreStart['docLinks']['links'];
+export type GetDocLinkFunction = (app: string, doc: string) => string;
+
+/**
+ * Creates links to the documentation.
+ *
+ * @see {@link DocLink} for a component that creates a link to the docs.
+ *
+ * @example
+ * ```typescript
+ *
+ * Learn what privileges individual roles grant.
+ *
+ * ```
+ *
+ * @example
+ * ```typescript
+ * const [docs] = useDocLinks();
+ *
+ *
+ * Learn how to get started with dashboards.
+ *
+ * ```
+ */
+export function useDocLinks(): [DocLinks, GetDocLinkFunction] {
+ const { services } = useKibana();
+ const { links, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = services.docLinks!;
+ const getDocLink = useCallback(
+ (app, doc) => {
+ return `${ELASTIC_WEBSITE_URL}guide/en/${app}/reference/${DOC_LINK_VERSION}/${doc}`;
+ },
+ [ELASTIC_WEBSITE_URL, DOC_LINK_VERSION]
+ );
+ return [links, getDocLink];
+}
+
+export interface DocLinkProps {
+ app: string;
+ doc: string;
+}
+
+export const DocLink: FunctionComponent = ({ app, doc, children }) => {
+ const [, getDocLink] = useDocLinks();
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/security/public/components/form_flyout.tsx b/x-pack/plugins/security/public/components/form_flyout.tsx
new file mode 100644
index 0000000000000..a0d397f81751e
--- /dev/null
+++ b/x-pack/plugins/security/public/components/form_flyout.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useEffect, FunctionComponent, RefObject } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiTitle,
+ EuiFlyout,
+ EuiFlyoutProps,
+ EuiFlyoutHeader,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButton,
+ EuiButtonProps,
+ EuiButtonEmpty,
+ EuiPortal,
+} from '@elastic/eui';
+import { useHtmlId } from './use_html_id';
+
+export interface FormFlyoutProps extends Omit {
+ title: string;
+ initialFocus?: RefObject;
+ onCancel(): void;
+ onSubmit(): void;
+ submitButtonText: string;
+ submitButtonColor?: EuiButtonProps['color'];
+ isLoading?: EuiButtonProps['isLoading'];
+ isDisabled?: EuiButtonProps['isDisabled'];
+}
+
+export const FormFlyout: FunctionComponent = ({
+ title,
+ submitButtonText,
+ submitButtonColor,
+ onCancel,
+ onSubmit,
+ isLoading,
+ isDisabled,
+ children,
+ initialFocus,
+ ...rest
+}) => {
+ useEffect(() => {
+ if (initialFocus && initialFocus.current) {
+ initialFocus.current.focus();
+ }
+ }, [initialFocus]);
+
+ const titleId = useHtmlId('formFlyout', 'title');
+
+ return (
+
+
+
+
+
{title}
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+ {submitButtonText}
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/security/public/components/use_current_user.ts b/x-pack/plugins/security/public/components/use_current_user.ts
new file mode 100644
index 0000000000000..b686e0ae9d778
--- /dev/null
+++ b/x-pack/plugins/security/public/components/use_current_user.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import useAsync from 'react-use/lib/useAsync';
+import constate from 'constate';
+import { AuthenticationServiceSetup } from '../authentication';
+
+export interface AuthenticationProviderProps {
+ authc: AuthenticationServiceSetup;
+}
+
+const [AuthenticationProvider, useAuthentication] = constate(
+ ({ authc }: AuthenticationProviderProps) => authc
+);
+
+export { AuthenticationProvider, useAuthentication };
+
+export function useCurrentUser() {
+ const authc = useAuthentication();
+ return useAsync(authc.getCurrentUser, [authc]);
+}
diff --git a/x-pack/plugins/security/public/components/use_form.ts b/x-pack/plugins/security/public/components/use_form.ts
new file mode 100644
index 0000000000000..33c7e184ec171
--- /dev/null
+++ b/x-pack/plugins/security/public/components/use_form.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ChangeEventHandler, FocusEventHandler, ReactEventHandler, useState } from 'react';
+import { get, set, cloneDeep, cloneDeepWith } from 'lodash';
+import useAsyncFn from 'react-use/lib/useAsyncFn';
+
+export type FormReturnTuple = [FormState, FormProps];
+
+export interface FormProps {
+ onSubmit: ReactEventHandler;
+ onChange: ChangeEventHandler;
+ onBlur: FocusEventHandler;
+}
+
+export interface FormOptions {
+ onSubmit: SubmitCallback;
+ validate: ValidateCallback;
+ defaultValues: Values;
+}
+
+/**
+ * Returns state and {@link HTMLFormElement} event handlers useful for creating
+ * forms with inline validation.
+ *
+ * @see {@link useFormState} if you don't want to use {@link HTMLFormElement}.
+ *
+ * @example
+ * ```typescript
+ * const [form, eventHandlers] = useForm({
+ * onSubmit: (values) => apiClient.create(values),
+ * validate: (values) => !values.email ? { email: 'Required' } : {}
+ * });
+ *
+ *
+ *
+ * Submit
+ *
+ * ```
+ */
+export function useForm(
+ options: FormOptions
+): FormReturnTuple {
+ const form = useFormState(options);
+
+ const eventHandlers: FormProps = {
+ onSubmit: (event) => {
+ event.preventDefault();
+ form.submit();
+ },
+ onChange: (event) => {
+ const { name, type, checked, value } = event.target;
+ if (name) {
+ form.setValue(name, type === 'checkbox' ? checked : value);
+ }
+ },
+ onBlur: (event) => {
+ const { name } = event.target;
+ if (name) {
+ form.setTouched(event.target.name);
+ }
+ },
+ };
+
+ return [form, eventHandlers];
+}
+
+export type FormValues = Record;
+export type SubmitCallback = (values: Values) => Promise;
+export type ValidateCallback = (
+ values: Values
+) => ValidationErrors | Promise>;
+export type ValidationErrors = DeepMap;
+export type TouchedFields = DeepMap;
+
+export interface FormState {
+ setValue(name: string, value: any): Promise;
+ setError(name: string, message: string): void;
+ setTouched(name: string): Promise;
+ reset(values: Values): void;
+ submit(): Promise;
+ values: Values;
+ errors: ValidationErrors;
+ touched: TouchedFields;
+ isValidating: boolean;
+ isSubmitting: boolean;
+ submitError: Error | undefined;
+ isInvalid: boolean;
+ isSubmitted: boolean;
+}
+
+/**
+ * Returns state useful for creating forms with inline validation.
+ *
+ * @example
+ * ```typescript
+ * const form = useFormState({
+ * onSubmit: (values) => apiClient.create(values),
+ * validate: (values) => !values.toggle ? { toggle: 'Required' } : {}
+ * });
+ *
+ * form.setValue('toggle', e.target.checked)}
+ * onBlur={() => form.setTouched('toggle')}
+ * isInvalid={!!form.errors.toggle}
+ * />
+ *
+ * Submit
+ *
+ * ```
+ */
+export function useFormState({
+ onSubmit,
+ validate,
+ defaultValues,
+}: FormOptions): FormState {
+ const [values, setValues] = useState(defaultValues);
+ const [errors, setErrors] = useState>({});
+ const [touched, setTouched] = useState>({});
+ const [submitCount, setSubmitCount] = useState(0);
+
+ const [validationState, validateForm] = useAsyncFn(
+ async (formValues: Values) => {
+ const nextErrors = await validate(formValues);
+ setErrors(nextErrors);
+ if (Object.keys(nextErrors).length === 0) {
+ setSubmitCount(0);
+ }
+ return nextErrors;
+ },
+ [validate]
+ );
+
+ const [submitState, submitForm] = useAsyncFn(
+ async (formValues: Values) => {
+ const nextErrors = await validateForm(formValues);
+ setTouched(mapDeep(formValues, true));
+ setSubmitCount(submitCount + 1);
+ if (Object.keys(nextErrors).length === 0) {
+ return onSubmit(formValues);
+ }
+ },
+ [validateForm, onSubmit]
+ );
+
+ return {
+ setValue: async (name, value) => {
+ const nextValues = setDeep(values, name, value);
+ setValues(nextValues);
+ await validateForm(nextValues);
+ },
+ setTouched: async (name, value = true) => {
+ setTouched(setDeep(touched, name, value));
+ await validateForm(values);
+ },
+ setError: (name, message) => {
+ setErrors(setDeep(errors, name, message));
+ setTouched(setDeep(touched, name, true));
+ },
+ reset: (nextValues) => {
+ setValues(nextValues);
+ setErrors({});
+ setTouched({});
+ setSubmitCount(0);
+ },
+ submit: () => submitForm(values),
+ values,
+ errors,
+ touched,
+ isValidating: validationState.loading,
+ isSubmitting: submitState.loading,
+ submitError: submitState.error,
+ isInvalid: Object.keys(errors).length > 0,
+ isSubmitted: submitCount > 0,
+ };
+}
+
+type DeepMap = {
+ [K in keyof T]?: T[K] extends any[]
+ ? T[K][number] extends object
+ ? Array>
+ : TValue
+ : T[K] extends object
+ ? DeepMap
+ : TValue;
+};
+
+function mapDeep(values: T, value: V): DeepMap {
+ return cloneDeepWith(values, (v) => {
+ if (typeof v !== 'object' && v !== null) {
+ return value;
+ }
+ });
+}
+
+function setDeep(values: T, name: string, value: V): T {
+ if (get(values, name) !== value) {
+ return set(cloneDeep(values), name, value);
+ }
+ return values;
+}
diff --git a/x-pack/plugins/security/public/components/use_html_id.ts b/x-pack/plugins/security/public/components/use_html_id.ts
new file mode 100644
index 0000000000000..23666e83cbf23
--- /dev/null
+++ b/x-pack/plugins/security/public/components/use_html_id.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useMemo } from 'react';
+import { htmlIdGenerator } from '@elastic/eui';
+
+/**
+ * Generates an ID that can be used for HTML elements.
+ *
+ * @param prefix Prefix of the id to be generated
+ * @param suffix Suffix of the id to be generated
+ *
+ * @example
+ * ```typescript
+ * const titleId = useHtmlId('changePasswordForm', 'title');
+ *
+ *
+ *