diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 5a9e8bc585119..525da9d832b53 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -381,8 +381,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib
**/*.scss @elastic/kibana-design
#CC# /packages/kbn-ui-framework/ @elastic/kibana-design
-# Core design
+# Core UI design
/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers
+/src/plugins/embeddable/**/*.scss @elastic/kibana-core-ui-designers
/x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers
/x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers
/x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers
diff --git a/docs/apm/images/advanced-discover.png b/docs/apm/images/advanced-discover.png
index 56ba58b2c1d41..5291526783a6b 100644
Binary files a/docs/apm/images/advanced-discover.png and b/docs/apm/images/advanced-discover.png differ
diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc
index 6c52c021fc0fc..7084777cbb6f9 100644
--- a/docs/apm/troubleshooting.asciidoc
+++ b/docs/apm/troubleshooting.asciidoc
@@ -157,7 +157,7 @@ the values in `http.request.cookies` are not indexed and thus not searchable.
*Ensure an index pattern exists*
As a first step, you should ensure the correct index pattern exists.
-In Kibana, navigate to *Management > Kibana > Index Patterns*.
+Open the main menu, then click *Stack Management > Index Patterns*.
In the pattern list, you should see an apm index pattern; The default is `apm-*`.
If you don't, the index pattern doesn't exist. See <> for information on how to fix this problem.
diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc
index 312391541a777..6456ba02bb8a8 100644
--- a/docs/canvas/canvas-tutorial.asciidoc
+++ b/docs/canvas/canvas-tutorial.asciidoc
@@ -14,7 +14,7 @@ For this tutorial, you'll need to add the <>,
and work with data in other contexts.
-To get started, open the menu, go to *Dev Tools*, then click *Painless Lab*.
+To get started, open the main menu, click *Dev Tools*, then click *Painless Lab*.
image::dev-tools/painlesslab/images/painless-lab.png[Painless Lab]
diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc
index eaa7fea6c7f8d..7cd54db5562b7 100644
--- a/docs/dev-tools/searchprofiler/getting-started.asciidoc
+++ b/docs/dev-tools/searchprofiler/getting-started.asciidoc
@@ -2,7 +2,7 @@
[[profiler-getting-started]]
=== Getting Started
-The {searchprofiler} is automatically enabled in {kib}. From the menu, go to *Dev Tools*, then click *Search Profiler*
+The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *Search Profiler*
to get started.
{searchprofiler} displays the names of the indices searched, the shards in each index,
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md
index d36ebd0745e8d..214c795fda9d1 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md
@@ -9,12 +9,13 @@ Constructs a new instance of the `IndexPatternsFetcher` class
Signature:
```typescript
-constructor(callDataCluster: LegacyAPICaller);
+constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean);
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
-| callDataCluster | LegacyAPICaller
| |
+| elasticsearchClient | ElasticsearchClient
| |
+| allowNoIndices | boolean
| |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md
index 52382372d6d96..addd29916d81d 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md
@@ -13,7 +13,7 @@ getFieldsForWildcard(options: {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
- allowNoIndices: boolean;
+ allow_no_indices: boolean;
};
}): Promise;
```
@@ -22,7 +22,7 @@ getFieldsForWildcard(options: {
| Parameter | Type | Description |
| --- | --- | --- |
-| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allowNoIndices: boolean;
};
}
| |
+| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
}
| |
Returns:
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md
index f71a702f3381d..3ba3c862bf16a 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md
@@ -14,7 +14,7 @@ export declare class IndexPatternsFetcher
| Constructor | Modifiers | Description |
| --- | --- | --- |
-| [(constructor)(callDataCluster)](./kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md) | | Constructs a new instance of the IndexPatternsFetcher
class |
+| [(constructor)(elasticsearchClient, allowNoIndices)](./kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md) | | Constructs a new instance of the IndexPatternsFetcher
class |
## Methods
diff --git a/docs/discover/images/Discover-Start.png b/docs/discover/images/Discover-Start.png
index fb885c20c1cf7..12ec2f9889bbd 100644
Binary files a/docs/discover/images/Discover-Start.png and b/docs/discover/images/Discover-Start.png differ
diff --git a/docs/discover/images/time-filter.png b/docs/discover/images/time-filter.png
new file mode 100644
index 0000000000000..f6d1d5809d7eb
Binary files /dev/null and b/docs/discover/images/time-filter.png differ
diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc
index ee1e1526f9d6f..3720a5b457d84 100644
--- a/docs/discover/search.asciidoc
+++ b/docs/discover/search.asciidoc
@@ -104,9 +104,7 @@ To save the current search:
. Click *Save* in the Kibana toolbar.
. Enter a name for the search and click *Save*.
-To import, export and delete saved searches:
-. Open the menu, then click *Stack Management.
-. From the {kib} menu, click *Saved Ojbects*.
+To import, export, and delete saved searches, open the main menu, then click *Stack Management > Saved Ojbects*.
==== Open a saved search
To load a saved search into Discover:
diff --git a/docs/discover/set-time-filter.asciidoc b/docs/discover/set-time-filter.asciidoc
index 93fdf9ffd695a..dcdc8ee791e83 100644
--- a/docs/discover/set-time-filter.asciidoc
+++ b/docs/discover/set-time-filter.asciidoc
@@ -30,7 +30,7 @@ to the last 15 minutes.
* *Refresh every* to specify an automatic refresh rate.
+
[role="screenshot"]
-image::images/Timepicker-View.png[Time filter menu]
+image::images/time-filter.png[Time filter menu]
. To set the start and end times, click the bar next to the time filter.
In the popup, select *Absolute*, *Relative* or *Now*, then specify the required
diff --git a/docs/fleet/images/fleet-start.png b/docs/fleet/images/fleet-start.png
index 60e5416fde127..0d0f7b8feec9c 100644
Binary files a/docs/fleet/images/fleet-start.png and b/docs/fleet/images/fleet-start.png differ
diff --git a/docs/getting-started/images/add-sample-data.png b/docs/getting-started/images/add-sample-data.png
index b8c2002b9c4cd..9dee27dcde71b 100644
Binary files a/docs/getting-started/images/add-sample-data.png and b/docs/getting-started/images/add-sample-data.png differ
diff --git a/docs/getting-started/images/tutorial-sample-dashboard.png b/docs/getting-started/images/tutorial-sample-dashboard.png
index 9f287640f201c..4c95c04c5e43e 100644
Binary files a/docs/getting-started/images/tutorial-sample-dashboard.png and b/docs/getting-started/images/tutorial-sample-dashboard.png differ
diff --git a/docs/getting-started/images/tutorial-sample-filter.png b/docs/getting-started/images/tutorial-sample-filter.png
index 7c1d041448557..56ebacadbef45 100644
Binary files a/docs/getting-started/images/tutorial-sample-filter.png and b/docs/getting-started/images/tutorial-sample-filter.png differ
diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc
index 6386feac5ab49..f239b7ae6ca88 100644
--- a/docs/getting-started/quick-start-guide.asciidoc
+++ b/docs/getting-started/quick-start-guide.asciidoc
@@ -10,8 +10,9 @@ When you've finished, you'll know how to:
* <>
[float]
-=== Before you begin
-When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. For more information, refer to {ref}/security-privileges.html[Security privileges].
+=== Required privileges
+When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices.
+For more information, refer to {ref}/security-privileges.html[Security privileges].
[float]
[[set-up-on-cloud]]
@@ -30,7 +31,7 @@ Sample data sets come with sample visualizations, dashboards, and more to help y
. On the *Sample eCommerce orders* card, click *Add data*.
+
[role="screenshot"]
-image::getting-started/images/add-sample-data.png[]
+image::getting-started/images/add-sample-data.png[Add data UI]
[float]
[[explore-the-data]]
@@ -38,7 +39,7 @@ image::getting-started/images/add-sample-data.png[]
*Discover* displays an interactive histogram that shows the distribution of of data, or documents, over time, and a table that lists the fields for each document that matches the index. By default, all fields are shown for each matching document.
-. Open the menu, then click *Discover*.
+. Open the main menu, then click *Discover*.
. Change the <> to *Last 7 days*.
+
@@ -70,7 +71,7 @@ For more information, refer to <>.
A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, Markdown, and more.
-. Open the menu, then click *Dashboard*.
+. Open the main menu, then click *Dashboard*.
. Click *[eCommerce] Revenue Dashboard*.
+
@@ -83,7 +84,7 @@ image::getting-started/images/tutorial-sample-dashboard.png[]
To focus in on the data you want to view on the dashboard, use filters.
-. From the *Controls* visualization, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*.
+. From the *[eCommerce] Controls* panel, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*.
+
For example, the following dashboard shows the data for women's clothing from Gnomehouse.
+
@@ -103,11 +104,11 @@ For more information, refer to <>.
[float]
[[create-a-visualization]]
-=== Create a visualization
+=== Create a visualization panel
-To create a treemap that shows the top regions and manufacturers, use *Lens*, then add the treemap to the dashboard.
+To create a treemap panel that shows the top regions and manufacturers, use *Lens*, then add the treemap panel to the dashboard.
-. From the {kib} toolbar, click *Edit*, then click *Create new*.
+. From the toolbar, click *Edit*, then click *Create new*.
. On the *New Visualization* window, click *Lens*.
@@ -126,7 +127,7 @@ image::getting-started/images/tutorial-visualization-dropdown.png[Visualization
. On the *Save Lens visualization*, enter a title and make sure *Add to Dashboard after saving* is selected, then click *Save and return*.
+
-The treemap appears as the last visualization on the dashboard.
+The treemap appears as the last visualization panel on the dashboard.
+
[role="screenshot"]
image::getting-started/images/tutorial-final-dashboard.gif[Final dashboard with new treemap visualization]
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 8e8d0e5bf996e..293597685ecc0 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -6,7 +6,7 @@ behavior of Kibana. For example, you can change the format used to display dates
specify the default index pattern, and set the precision for displayed decimal
values.
-. Open the menu, then go to *Stack Management > {kib} > Advanced Settings*.
+. Open the main menu, then click *Stack Management > Advanced Settings*.
. Scroll or search for the setting you want to modify.
. Enter a new value for the setting.
. Click *Save changes*.
diff --git a/docs/management/alerting/alerts-and-actions-intro.asciidoc b/docs/management/alerting/alerts-and-actions-intro.asciidoc
index 429d7915cc1c3..0c7ca7f1db17d 100644
--- a/docs/management/alerting/alerts-and-actions-intro.asciidoc
+++ b/docs/management/alerting/alerts-and-actions-intro.asciidoc
@@ -6,8 +6,8 @@ beta[]
The *Alerts and Actions* UI lets you <> in a space, and provides tools to <> so that alerts can trigger actions like notification, indexing, and ticketing.
-To manage alerting and connectors, open the menu,
-then go to *Stack Management > Alerts and Insights > Alerts and Actions*.
+To manage alerting and connectors, open the main menu,
+then click *Stack Management > Alerts and Insights > Alerts and Actions*.
[role="screenshot"]
image:management/alerting/images/alerts-and-actions-ui.png[Example alert listing in the Alerts and Actions UI]
diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc
index 7de2a042160e9..e83e6d262f26c 100644
--- a/docs/management/index-patterns.asciidoc
+++ b/docs/management/index-patterns.asciidoc
@@ -25,8 +25,8 @@ image::images/management-index-read-only-badge.png[Example of Index Pattern Mana
[[settings-create-pattern]]
=== Create an index pattern
-When you don't have an index pattern, {kib} prompts you to create one. Or, you can open the menu,
-then go to *Stack Management > {kib} > Index Patterns* to go directly to the *Index Patterns* UI.
+When you don't have an index pattern, {kib} prompts you to create one. Or, you can open the main menu,
+then click *Stack Management > Index Patterns*.
[role="screenshot"]
image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollup index pattern"]
diff --git a/docs/management/index-patterns/images/index-pattern-ui.png b/docs/management/index-patterns/images/index-pattern-ui.png
new file mode 100644
index 0000000000000..7d16540aa03a2
Binary files /dev/null and b/docs/management/index-patterns/images/index-pattern-ui.png differ
diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc
index 7986e4e56279a..d9745bfef524a 100644
--- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc
+++ b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc
@@ -7,7 +7,7 @@ pipelines that perform common transformations and
enrichments on your data. For example, you might remove a field,
rename an existing field, or set a new field.
-You’ll find *Ingest Node Pipelines* in *Stack Management > Ingest*. With this feature, you can:
+To begin, open the main menu, then click *Stack Management > Ingest Node Pipelines*. With *Ingest Node Pipelines*, you can:
* View a list of your pipelines and drill down into details.
* Create a pipeline that defines a series of tasks, known as processors.
@@ -23,7 +23,7 @@ image:management/ingest-pipelines/images/ingest-pipeline-list.png["Ingest node p
The minimum required permissions to access *Ingest Node Pipelines* are
the `manage_pipeline` and `cluster:monitor/nodes/info` cluster privileges.
-You can add these privileges in *Stack Management > Security > Roles*.
+To add privileges, open the main menu, then click *Stack Management > Roles*.
[role="screenshot"]
image:management/ingest-pipelines/images/ingest-pipeline-privileges.png["Privileges required for Ingest Node Pipelines"]
diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc
index 678e160b99af0..10c98cca26345 100644
--- a/docs/management/managing-beats.asciidoc
+++ b/docs/management/managing-beats.asciidoc
@@ -4,7 +4,7 @@
include::{asciidoc-dir}/../../shared/discontinued.asciidoc[tag=cm-discontinued]
-To use {beats} Central Management UI, open the menu, go to *Stack Management > Ingest >
+To use {beats} Central Management, open the main menu, click *Stack Management >
{beats} Central Management*, then define and
manage configurations in a central location in {kib} and quickly deploy
configuration changes to all {beats} running across your enterprise. For more
@@ -18,8 +18,8 @@ about central management, see the related {beats} documentation:
This feature requires an Elastic license that includes {beats} central
management.
-Don't have a license? You can start a 30-day trial. Open the menu,
-go to *Stack Management > Stack > License Management*. At the end of the trial
+Don't have a license? You can start a 30-day trial. Open the main menu, then
+click *Stack Management > License Management*. At the end of the trial
period, you can purchase a subscription to keep using central management. For
more information, see https://www.elastic.co/subscriptions and
<>.
diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc
index ad3a0ef0fcdd1..441bce43c7cdf 100644
--- a/docs/management/managing-fields.asciidoc
+++ b/docs/management/managing-fields.asciidoc
@@ -134,7 +134,7 @@ https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless
[[create-scripted-field]]
=== Create a scripted field
-. Open the menu, then go to *Stack Management > {kib} > Index Patterns*
+. Open the main menu, then click *Stack Management > Index Patterns*.
. Select the index pattern you want to add a scripted field to.
. Go to the *Scripted fields* tab for the index pattern, then click *Add scripted field*.
. Enter a name for the scripted field.
diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc
index b199e076443ab..8416c164c6c51 100644
--- a/docs/management/managing-indices.asciidoc
+++ b/docs/management/managing-indices.asciidoc
@@ -12,15 +12,7 @@ way possible.
This page shows you how to use *Index Management* features to:
-* View and edit index settings.
-* View mappings and statistics for an index.
-* Perform index-level operations, such as refreshes and freezes.
-* View and manage data streams.
-* Create index templates to automatically configure new data streams and
-indices.
-
-To manage your indices, open the menu, then click *Stack Management > Index
-Management*.
+To manage your indices, open the main menu, then click *Stack Management > Index Management*.
[role="screenshot"]
image::images/management_index_labels.png[Index Management UI]
diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc
index b53bda95466dc..8944414f6bfbc 100644
--- a/docs/management/managing-licenses.asciidoc
+++ b/docs/management/managing-licenses.asciidoc
@@ -7,7 +7,7 @@ with no expiration date. For the full list of features, refer to
If you want to try out the full set of features, you can activate a free 30-day
trial. To view the status of your license, start a trial, or install a new
-license, open the menu, then go to *Stack Management > Stack > License Management*.
+license, open the main menu, then click *Stack Management > License Management*.
NOTE: You can start a trial only if your cluster has not already activated a
trial license for the current major product version. For example, if you have
@@ -34,7 +34,7 @@ the features that will no longer be supported if you revert to a basic license.
The `manage` cluster privilege is required to access *License Management*.
-You can add this privilege in *Stack Management > Security > Roles*.
+To add the privilege, open the main menu, then click *Stack Management > Roles*.
[discrete]
[[update-license]]
diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc
index 8c885ddca52e5..639be87c540fb 100644
--- a/docs/management/managing-saved-objects.asciidoc
+++ b/docs/management/managing-saved-objects.asciidoc
@@ -5,13 +5,7 @@ The *Saved Objects* UI helps you keep track of and manage your saved objects. Th
store data for later use, including dashboards, visualizations, maps, index patterns,
Canvas workpads, and more.
-To get started, open the menu, then go to *Stack Management > {kib} > Saved Objects*. With this UI, you can:
-
-* <>
-* <>
-* <>
-* <>
-
+To get started, open the main menu, then click *Stack Management > Saved Objects*.
[role="screenshot"]
image::images/management-saved-objects.png[Saved Objects]
diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc
index 7324f45594bd7..bc876ab67bc62 100644
--- a/docs/management/rollups/create_and_manage_rollups.asciidoc
+++ b/docs/management/rollups/create_and_manage_rollups.asciidoc
@@ -8,11 +8,7 @@ by an index pattern, and then rolls it into a new index. Rollup indices are a go
compactly store months or years of historical
data for use in visualizations and reports.
-To get started, open the menu, then go to *Stack Management > Data > Rollup Jobs*. With this UI,
-you can:
-
-* <>
-* <>
+To get started, open the main menu, then click *Stack Management > Rollup Jobs*.
[role="screenshot"]
image::images/management_rollup_list.png[][List of currently active rollup jobs]
@@ -25,7 +21,7 @@ Before using this feature, you should be familiar with how rollups work.
The `manage_rollup` cluster privilege is required to access *Rollup jobs*.
-You can add this privilege in *Stack Management > Security > Roles*.
+To add the privilege, open the main menu, then click *Stack Management > Roles*.
[float]
[[create-and-manage-rollup-job]]
@@ -137,7 +133,7 @@ Your next step is to visualize your rolled up data in a vertical bar chart.
Most visualizations support rolled up data, with the exception of Timelion and Vega visualizations.
-. Go to *Stack Management > {kib} > Index Patterns*.
+. Open the main menu, then click *Stack Management > Index Patterns*.
. Click *Create index pattern*, and select *Rollup index pattern* from the dropdown.
+
@@ -152,7 +148,7 @@ is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_log
matches the rolled up index pattern and `kibana_sample_data_logs` matches the index
pattern for raw data.
-. Go to *Dashboard* and create a vertical bar chart.
+. Open the main menu, click *Dashboard*, then create and add a vertical bar chart.
. Choose `rollup_logstash,kibana_sample_data_logs`
as your source to see both the raw and rolled up data.
diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc
index 1bf62522e245c..62633441ef161 100644
--- a/docs/management/snapshot-restore/index.asciidoc
+++ b/docs/management/snapshot-restore/index.asciidoc
@@ -8,7 +8,7 @@ Snapshots are important because they provide a copy of your data in case
something goes wrong. If you need to roll back to an older version of your data,
you can restore a snapshot from the repository.
-To get started, open the menu, then go to *Stack Management > Data > Snapshot and Restore*.
+To get started, open the main menu, then click *Stack Management > Snapshot and Restore*.
With this UI, you can:
* Register a repository for storing your snapshots
@@ -32,7 +32,7 @@ The minimum required permissions to access *Snapshot and Restore* include:
* Cluster privileges: `monitor`, `manage_slm`, `cluster:admin/snapshot`, and `cluster:admin/repository`
* Index privileges: `all` on the `monitor` index if you want to access content in the *Restore Status* tab
-To add privileges, open the menu, then go to *Stack Management > Security > Roles*.
+To add privileges, open the main menu, then click *Stack Management > Roles*.
[role="screenshot"]
image:management/snapshot-restore/images/snapshot_permissions.png["Edit Role"]
@@ -191,7 +191,7 @@ your master and data nodes. You can do this in one of two ways:
Use *Snapshot and Restore* to register the repository where your snapshots
will live.
-. Open the menu, then go to *Stack Management > Data > Snapshot and Restore*.
+. Open the main menu, then click *Stack Management > Snapshot and Restore*.
. Click *Register a repository* in either the introductory message or *Repository view*.
. Enter a name for your repository, for example, `my_backup`.
. Select *Shared file system*.
@@ -212,7 +212,7 @@ The repository currently doesn’t have any snapshots.
==== Add a snapshot to the repository
Use the {ref}/snapshots-take-snapshot.html[snapshot API] to create a snapshot.
-. Open the menu, go to *Dev Tools*, then select *Console*.
+. Open the main menu, click *Dev Tools*, then select *Console*.
. Create the snapshot:
+
[source,js]
diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc
index 2b8c2da2ef577..61df6457a9bde 100644
--- a/docs/management/upgrade-assistant/index.asciidoc
+++ b/docs/management/upgrade-assistant/index.asciidoc
@@ -4,7 +4,7 @@
The Upgrade Assistant helps you prepare for your upgrade to the next major {es} version.
For example, if you are using 6.8, the Upgrade Assistant helps you to upgrade to 7.0.
-To access the assistant, open the menu, then go to *Stack Management > Stack > Upgrade Assistant*.
+To access the assistant, open the main menu, then click *Stack Management > Upgrade Assistant*.
The assistant identifies the deprecated settings in your cluster and indices
and guides you through the process of resolving issues, including reindexing.
@@ -19,7 +19,7 @@ For example, if you want to upgrade to to 7.0, make sure that you are using 6.8.
The `manage` cluster privilege is required to access the *Upgrade assistant*.
Additional privileges may be needed to perform certain actions.
-You can add this privilege in *Stack Management > Security > Roles*.
+To add the privilege, open the main menu, then click *Stack Management > Roles*.
[float]
=== Reindexing
diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc
index 23a0acbff5718..69c33aa7a1dac 100644
--- a/docs/management/watcher-ui/index.asciidoc
+++ b/docs/management/watcher-ui/index.asciidoc
@@ -8,8 +8,8 @@ Watches are helpful for analyzing mission-critical and business-critical
streaming data. For example, you might watch application logs for performance
outages or audit access logs for security threats.
-To get started with the Watcher UI, open then menu,
-then go to *Stack Management > Alerts and Insights > Watcher*.
+To get started, open then main menu,
+then click *Stack Management > Watcher*.
With this UI, you can:
* <>
@@ -41,7 +41,7 @@ and either of these watcher roles:
* `watcher_admin`. You can perform all Watcher actions, including create and edit watches.
* `watcher_user`. You can view watches, but not create or edit them.
-To manage roles, open then menu, then go to *Stack Management > Security > Roles*, or use the
+To manage roles, open then main menu, then click *Stack Management > Roles*, or use the
<>. Watches are shared between
all users with the same role.
diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc
new file mode 100644
index 0000000000000..3c9bea11176cc
--- /dev/null
+++ b/docs/maps/geojson-upload.asciidoc
@@ -0,0 +1,44 @@
+[role="xpack"]
+[[geojson-upload]]
+== Upload GeoJSON data
+
+Maps makes it easy to import geospatial data into the Elastic Stack.
+Using the GeoJSON Upload feature, you can drag and drop your point and shape
+data files directly into {es}, and then use them as layers
+in the map. You can also use the GeoJSON data in the broader Kibana ecosystem,
+for example, in visualizations and Canvas workpads.
+
+[float]
+=== Why GeoJSON?
+GeoJSON is an open-standard file format for storing geospatial vector data.
+Although many vector data formats are available in the GIS community,
+GeoJSON is the most commonly used and flexible option.
+[float]
+
+=== Upload a GeoJSON file
+Follow these instructions to upload a GeoJSON data file, or try the
+<>.
+
+. Open the main menu, click *Maps*, and then click *Add layer*.
+. Click *Uploaded GeoJSON*.
++
+[role="screenshot"]
+image::maps/images/fu_gs_select_source_file_upload.png[]
+
+. Use the file chooser to select a valid GeoJSON file. The file will load
+a preview of the data on the map.
+. Use the default *Index type* of {ref}/geo-point.html[geo_point] for point data,
+or override it and select {ref}/geo-shape.html[geo_shape].
+All other shapes will default to a type of `geo_shape`.
+. Leave the default *Index name* and *Index pattern* names (the name of the uploaded
+file minus its extension). You might need to change the index name if it is invalid.
+. Click *Import file*.
++
+Upon completing the indexing process and creating the associated index pattern,
+the Elasticsearch responses are shown on the *Layer add panel* and the indexed data
+appears on the map. The geospatial data on the map
+should be identical to the locally-previewed data, but now it's indexed data from Elasticsearch.
+
+. To continue adding data to the map, click *Add layer*.
+. In *Layer settings*, adjust any settings or <> as needed.
+. Click *Save & close*.
diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc
index 194d09c491cee..ff0c9bf1f72ba 100644
--- a/docs/maps/import-geospatial-data.asciidoc
+++ b/docs/maps/import-geospatial-data.asciidoc
@@ -11,7 +11,7 @@ Choose an import tool based on the format of your geospatial data.
*File Data Visualizer* indexes CSV files with latitude and longitude columns as a geo_point.
-. Open the side navigation menu, and click *Machine Learning*.
+. Open the main menu, then click *Machine Learning*.
. Select the *Data Visualizer* tab, then click *Upload file*.
. Use the file chooser to select a CSV file.
. Click *Import*.
diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc
index f48ff268755d2..5c6cd87b235e1 100644
--- a/docs/maps/maps-getting-started.asciidoc
+++ b/docs/maps/maps-getting-started.asciidoc
@@ -50,7 +50,7 @@ In this tutorial, you'll learn to:
The first thing to do is to create a new map.
-. If you haven't already, click *{kib} > Maps* from the side navigation.
+. If you haven't already, open the main menu, then click *Maps*.
. On the maps list page, click *Create map*.
. Set the time range to *Last 7 days*.
+
@@ -188,7 +188,7 @@ You have completed the steps for re-creating the sample data map.
=== Add the map to a dashboard
You can add your saved map to a {kibana-ref}/dashboard.html[dashboard] and view your geospatial data alongside bar charts, pie charts, and other visualizations.
-. Open the menu, then go to *Dashboard*.
+. Open the main menu, then click *Dashboard*.
. Click *Create dashboard*.
. Set the time range to *Last 7 days*.
. Click *Add*.
diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc
index b396c40aa21f9..9054a97c90496 100644
--- a/docs/settings/apm-settings.asciidoc
+++ b/docs/settings/apm-settings.asciidoc
@@ -18,7 +18,7 @@ It is enabled by default.
// Any changes made in this file will be seen there as well.
// tag::apm-indices-settings[]
-Index defaults can be changed in Kibana. Navigate to *APM* > *Settings* > *Indices*.
+Index defaults can be changed in Kibana. Open the main menu, then click *APM > Settings > Indices*.
Index settings in the APM app take precedence over those set in `kibana.yml`.
[role="screenshot"]
@@ -44,7 +44,7 @@ Changing these settings may disable features of the APM App.
| Set to `false` to disable the APM app. Defaults to `true`.
| `xpack.apm.ui.enabled` {ess-icon}
- | Set to `false` to hide the APM app from the menu. Defaults to `true`.
+ | Set to `false` to hide the APM app from the main menu. Defaults to `true`.
| `xpack.apm.ui.transactionGroupBucketSize`
| Number of top transaction groups displayed in the APM app. Defaults to `1000`.
diff --git a/docs/setup/access.asciidoc b/docs/setup/access.asciidoc
index 49aa411e91512..edf936fe54267 100644
--- a/docs/setup/access.asciidoc
+++ b/docs/setup/access.asciidoc
@@ -1,24 +1,37 @@
[[access]]
== Access {kib}
-Kibana is a web application that you access through port 5601. All you need to do is point your web browser at the
-machine where Kibana is running and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`.
-If you want to allow remote users to connect, set the parameter `server.host` in `kibana.yml` to a non-loopback address.
+The fastest way to access {kib} is to use our hosted {es} Service. If you <>, access {kib} through the web application.
-When you access Kibana, the <> page loads by default with the default index pattern selected. The
-time filter is set to the last 15 minutes and the search query is set to match-all (\*).
+[float]
+=== Set up on cloud
-If you don't see any documents, try setting the time filter to a wider time range.
-If you still don't see any results, it's possible that you don't *have* any documents.
+include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[]
+
+[float]
+[[log-on-to-the-web-application]]
+=== Log on to the web application
+
+If you are using a self-managed deployment, you access {kib} through the web application on port 5601.
+
+. Point your web browser to the machine where you are running {kib} and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`.
+
+. To allow remote users to connect to {kib}, set the parameter `server.host` in kibana.yml to a non-loopback address.
+
+. On the home page, click *{kib}*.
++
+To make the {kib} page your landing page, click *Make this my landing page*.
[float]
[[status]]
-=== Check {kib} status
+=== Check the {kib} status
-You can reach the Kibana server's status page by navigating to the status endpoint, for example, `localhost:5601/status`. The status page displays
-information about the server's resource usage and lists the installed plugins.
+To view the {kib} status page, use the status endpoint. For example, `localhost:5601/status`. The status page displays
+information about the server resource usage and installed plugins.
[role="screenshot"]
image::images/kibana-status-page-7_5_0.png[]
-NOTE: For JSON-formatted server status details, use the API endpoint at `localhost:5601/api/status`
+For JSON-formatted server status details, use the `localhost:5601/api/status` API endpoint.
+
+
diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc
index 3db562319641c..c968ca6f35029 100644
--- a/docs/setup/connect-to-elasticsearch.asciidoc
+++ b/docs/setup/connect-to-elasticsearch.asciidoc
@@ -20,14 +20,15 @@ to see all that you can do in {kib}.
experimental[]
-To visualize data in a CSV, JSON, or log file, you can upload it using the File
-Data Visualizer. On the home page, click *Import a CSV, NDSON, or log file*, and
-then drag your file into the File Data Visualizer. Alternatively, you can open
-it by navigating to *Machine Learning* from the side navigation and selecting
+To visualize data in a CSV, JSON, or log file, you can upload it using the File
+Data Visualizer. On the home page, click *Upload a file*, and
+then drag your file onto the *File Data Visualizer*. Alternatively, you can open
+it by navigating to *Machine Learning* from the side navigation and selecting
+
*Data Visualizer*.
[role="screenshot"]
-image::images/data-viz-homepage.jpg[File Data Visualizer on the home page]
+image::images/ingest-data.png[File Data Visualizer on the home page]
You can upload a file up to 100 MB. This value is configurable up to 1 GB in
<>.
@@ -78,7 +79,7 @@ create an index pattern that matches the names of the indices that you want to e
When you add data with the File Data Visualizer, GeoJSON Upload feature,
or built-in tutorial, an index pattern is created for you.
-. Go to *Stack Management*, and then click *Index Patterns*.
+. Open the main menu, then click *Stack Management > Index Patterns*.
. Click *Create index pattern*.
diff --git a/docs/setup/images/data-viz-homepage.jpg b/docs/setup/images/data-viz-homepage.jpg
deleted file mode 100644
index f7a952b65d41f..0000000000000
Binary files a/docs/setup/images/data-viz-homepage.jpg and /dev/null differ
diff --git a/docs/setup/images/ingest-data.png b/docs/setup/images/ingest-data.png
new file mode 100644
index 0000000000000..b1943d6de27d2
Binary files /dev/null and b/docs/setup/images/ingest-data.png differ
diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc
index 9e505b8bfe045..1bc781e1dda49 100644
--- a/docs/spaces/index.asciidoc
+++ b/docs/spaces/index.asciidoc
@@ -29,7 +29,7 @@ Kibana supports spaces in several ways. You can:
[[spaces-managing]]
=== View, create, and delete spaces
-Open the menu, then go to *Stack Management > {kib} > Spaces* for an overview of your spaces. This view provides actions
+Open the main menu, then click *Stack Management > Spaces* for an overview of your spaces. This view provides actions
for you to create, edit, and delete spaces.
[role="screenshot"]
@@ -94,8 +94,8 @@ image::spaces/images/spaces-roles.png["Controlling features visiblity"]
[[spaces-moving-objects]]
=== Move saved objects between spaces
-To <> from one space to another, open the menu,
-then go to *Stack Management > {kib} > Saved objects*.
+To <> from one space to another, open the main menu,
+then click *Stack Management > Saved Objects*.
Alternately, you can move objects using {kib}'s <>
interface.
diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc
index 9301224e6df48..aad192dbddb30 100644
--- a/docs/user/alerting/action-types/pagerduty.asciidoc
+++ b/docs/user/alerting/action-types/pagerduty.asciidoc
@@ -89,8 +89,8 @@ image::user/alerting/images/pagerduty-integration.png[PagerDuty Integrations tab
+
* Create a connector as part of creating an alert by selecting PagerDuty in the *Actions*
section of the alert configuration and selecting *Add new*.
-* Alternatively, create a connector by navigating to *Management* from the {kib} navbar and selecting
-*Alerts and Actions*. Then, select the *Connectors* tab, click the *Create connector* button, and select the PagerDuty option.
+* Alternatively, create a connector. To create a connector, open the main menu, click *Stack Management* >
+Alerts and Actions*, select *Connectors*, click *Create connector*, then select the PagerDuty option.
. Configure the connector by giving it a name and entering the Integration Key, optionally entering a custom API URL.
+
@@ -99,7 +99,7 @@ See <> for how to obtain the endpoint and
. Save the Connector.
-. Create an alert using *Management > Alerts and Actions* or the application of your choice.
+. To create an alert, open the main menu, then click *Stack Management > Alerts and Actions* or the application of your choice.
. Set up an action using your PagerDuty connector, by determining:
+
@@ -120,7 +120,7 @@ To remove a PagerDuty connector from an alert, simply remove it
from the *Actions* section of that alert, using the remove (x) icon.
This will disable the integration for the particular alert.
-To delete the connector entirely, go to *Management > Alerts and Actions*.
+To delete the connector entirely, open the main menu, then click *Stack Management > Alerts and Actions*.
Select the *Connectors* tab, and then click on the delete icon.
This is an irreversible action and impacts all alerts that use this connector.
diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc
index e3f1703f08e88..722607ac05f87 100644
--- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc
+++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc
@@ -61,7 +61,7 @@ Sensitive properties, such as passwords, can also be stored in the < {kib} > Alerts and Actions*, preconfigured connectors
+When you open the main menu, click *Stack Management > Alerts and Actions*. Preconfigured connectors
appear on the <>,
regardless of which space you are in.
They are tagged as “preconfigured”, and you cannot delete them.
@@ -101,7 +101,7 @@ This example shows a preconfigured action type with one out-of-the box connector
[[managing-pre-configured-action-types]]
To attach a preconfigured action to an alert:
-. Open the menu, then go to *Stack Management > {kib} > Alerts and Actions*, open the *Connectors* tab.
+. Open the main menu, click *Stack Management > Alerts and Actions*, then open the *Connectors* tab.
. Click *Create connector.*
diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc
index 297dfac5b10bd..c10641bb3a6b9 100644
--- a/docs/user/canvas.asciidoc
+++ b/docs/user/canvas.asciidoc
@@ -17,7 +17,7 @@ With Canvas, you can:
* Focus the data you want to display with filters.
-To begin, open the menu, then go to *Canvas*.
+To begin, open the main menu, then click *Canvas*.
[role="screenshot"]
image::images/canvas-gs-example.png[Getting started example]
diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc
index 4fa4f9860c2bd..5fda1af55c7fe 100644
--- a/docs/user/dashboard/dashboard.asciidoc
+++ b/docs/user/dashboard/dashboard.asciidoc
@@ -8,7 +8,7 @@ A _dashboard_ is a collection of panels that you use to analyze your data. On a
you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations,
interactive controls, markdown, and more.
-With *Dashboard*s, you can:
+With *Dashboard*, you can:
* Add multiple panels to see many aspects and views of your data in one place.
@@ -22,7 +22,7 @@ With *Dashboard*s, you can:
* Generate reports based on your findings.
-To begin, open the menu, go to *Dashboard*, then click *Create dashboard*.
+To begin, open the main menu, click *Dashboard*, then click *Create dashboard*.
[role="screenshot"]
image:images/Dashboard_example.png[Example dashboard]
@@ -424,33 +424,37 @@ Ready to try out Timelion? For step-by-step tutorials, refer to:
[[timelion-deprecation]]
==== Timelion app deprecation
-Deprecated since 7.0, the Timelion app will be removed in 8.0. If you have any Timelion worksheets, you must migrate them to a dashboard.
+In 7.0 and later, *Timelion* app is deprecated. In 8.0 and later, *Timelion* app is removed from {kib}. To prepare for the removal of *Timelion* app, you must migrate *Timelion* app worksheets to a dashboard.
-NOTE: Only the Timelion app is deprecated. {kib} continues to support Timelion
-visualizations on dashboards and in Visualize and Canvas.
+NOTE: Only *Timelion* app is deprecated. {kib} continues to support *Timelion*
+visualizations in *Dashboard*, *Visualize*, and *Canvas*.
-To migrate a Timelion worksheet to a dashboard:
+To migrate a *Timelion* worksheet to a dashboard:
-. Open the menu, click **Dashboard**, then click **Create dashboard**.
+. Open the main menu, click *Dashboard*, then click *Create dashboard*.
-. On the dashboard, click **Create New**, then select the Timelion visualization.
+. For each *Timelion* app worksheet, complete the following steps.
-. On a new tab, open the Timelion app, select the chart you want to copy, and copy its expression.
+.. On the dashboard, click *Create New*, then click *Timelion* on the *New Visualization* window.
+
+.. Open a new tab, open the *Timelion* app, select the chart you want to copy, then copy the chart expression.
+
[role="screenshot"]
-image::images/timelion-copy-expression.png[]
+image::images/timelion-copy-expression.png[Timelion app chart]
-. Return to the other tab and paste the copied expression to the *Timelion Expression* field and click **Update**.
+.. Go to *Timelion*, paste the chart expression in the *Timelion expression* field, then click *Update*.
+
[role="screenshot"]
-image::images/timelion-vis-paste-expression.png[]
+image::images/timelion-vis-paste-expression.png[Timelion advanced editor UI]
+
+.. In the toolbar, click *Save*.
-. Save the new visualization, give it a name, and click **Save and Return**.
+.. On the *Save visualization* window, enter the visualization *Title*, then click *Save and return*.
+
-Your Timelion visualization will appear on the dashboard. Repeat this for all your charts on each worksheet.
+The Timelion visualization panel appears on the dashboard.
+
[role="screenshot"]
-image::images/timelion-dashboard.png[]
+image::images/timelion-dashboard.png[Final dashboard with saved Timelion app worksheet]
[float]
[[save-panels]]
@@ -458,7 +462,7 @@ image::images/timelion-dashboard.png[]
When you’ve finished making changes, save the panels.
-. Click *Save*.
+. In the toolbar, click *Save*.
. Add the *Title* and optional *Description*.
. Click *Save and return*.
diff --git a/docs/user/dashboard/edit-dashboards.asciidoc b/docs/user/dashboard/edit-dashboards.asciidoc
index 7534ea1e9e9fb..7b712b355b315 100644
--- a/docs/user/dashboard/edit-dashboards.asciidoc
+++ b/docs/user/dashboard/edit-dashboards.asciidoc
@@ -78,7 +78,7 @@ Put the dashboard in *Edit* mode, then use the following options:
* To resize, click the resize control, then drag to the new dimensions.
-* To delete, open the panel menu, then select Delete from dashboard. When you delete a panel from the dashboard, the
+* To delete, open the panel menu, then select *Delete from dashboard*. When you delete a panel from the dashboard, the
visualization or saved search from the panel is still available in Kibana.
[float]
diff --git a/docs/user/dashboard/images/Dashboard_add_new_visualization.png b/docs/user/dashboard/images/Dashboard_add_new_visualization.png
index 3685f9c5c9a74..5f73b2f1adde2 100644
Binary files a/docs/user/dashboard/images/Dashboard_add_new_visualization.png and b/docs/user/dashboard/images/Dashboard_add_new_visualization.png differ
diff --git a/docs/user/dashboard/images/Dashboard_add_visualization.png b/docs/user/dashboard/images/Dashboard_add_visualization.png
index b1b86d47e5982..4caa34ef3d082 100644
Binary files a/docs/user/dashboard/images/Dashboard_add_visualization.png and b/docs/user/dashboard/images/Dashboard_add_visualization.png differ
diff --git a/docs/user/dashboard/images/Dashboard_example.png b/docs/user/dashboard/images/Dashboard_example.png
index 1a80f4b3bdf07..c2e338d0fd31b 100644
Binary files a/docs/user/dashboard/images/Dashboard_example.png and b/docs/user/dashboard/images/Dashboard_example.png differ
diff --git a/docs/user/dashboard/images/Dashboard_inspect.png b/docs/user/dashboard/images/Dashboard_inspect.png
index d65b968e043a6..635eef4a017f6 100644
Binary files a/docs/user/dashboard/images/Dashboard_inspect.png and b/docs/user/dashboard/images/Dashboard_inspect.png differ
diff --git a/docs/user/dashboard/images/drilldown_on_piechart.gif b/docs/user/dashboard/images/drilldown_on_piechart.gif
index c9b3311df0325..c438e14371887 100644
Binary files a/docs/user/dashboard/images/drilldown_on_piechart.gif and b/docs/user/dashboard/images/drilldown_on_piechart.gif differ
diff --git a/docs/user/dashboard/images/timelion-copy-expression.png b/docs/user/dashboard/images/timelion-copy-expression.png
new file mode 100644
index 0000000000000..a9c3afe9b060f
Binary files /dev/null and b/docs/user/dashboard/images/timelion-copy-expression.png differ
diff --git a/docs/visualize/images/timelion-vis-paste-expression.png b/docs/user/dashboard/images/timelion-vis-paste-expression.png
similarity index 100%
rename from docs/visualize/images/timelion-vis-paste-expression.png
rename to docs/user/dashboard/images/timelion-vis-paste-expression.png
diff --git a/docs/user/dashboard/images/url_drilldown_go_to_github.gif b/docs/user/dashboard/images/url_drilldown_go_to_github.gif
index 7cca3f72d5a68..3a3b00dc0e2ce 100644
Binary files a/docs/user/dashboard/images/url_drilldown_go_to_github.gif and b/docs/user/dashboard/images/url_drilldown_go_to_github.gif differ
diff --git a/docs/user/dashboard/share-dashboards.asciidoc b/docs/user/dashboard/share-dashboards.asciidoc
index cfa146d60fdac..6c05240c934e8 100644
--- a/docs/user/dashboard/share-dashboards.asciidoc
+++ b/docs/user/dashboard/share-dashboards.asciidoc
@@ -23,5 +23,5 @@ tools. To create a short URL, you must have write access to {kib}.
[[import-dashboards]]
=== Export the dashboard
-To export the dashboard, open the menu, then click *Stack Management > Saved Objects*. For more information,
+To export the dashboard, open the main menu, then click *Stack Management > Saved Objects*. For more information,
refer to <>.
\ No newline at end of file
diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc
index aca6d40a3532e..086c0707b3c2c 100644
--- a/docs/user/graph/getting-started.asciidoc
+++ b/docs/user/graph/getting-started.asciidoc
@@ -9,7 +9,7 @@ You must index data into {es} before you can create a graph.
[[exploring-connections]]
=== Graph a data connection
-. Open the menu, then go to *Graph*.
+. Open the main menu, then click *Graph*.
+
If this is your first graph, follow the prompts to create it.
For subsequent graphs, click *New*.
diff --git a/docs/user/graph/images/graph-add-query.png b/docs/user/graph/images/graph-add-query.png
index 1b233e3ef8b69..93ddf6a6132f4 100644
Binary files a/docs/user/graph/images/graph-add-query.png and b/docs/user/graph/images/graph-add-query.png differ
diff --git a/docs/user/graph/images/graph-link-summary.png b/docs/user/graph/images/graph-link-summary.png
index 4c75be00de0f5..a3dfdc0f79d96 100644
Binary files a/docs/user/graph/images/graph-link-summary.png and b/docs/user/graph/images/graph-link-summary.png differ
diff --git a/docs/user/graph/images/graph-url-connections.png b/docs/user/graph/images/graph-url-connections.png
index 4f8c163ab764b..34b57d489b048 100644
Binary files a/docs/user/graph/images/graph-url-connections.png and b/docs/user/graph/images/graph-url-connections.png differ
diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc
index 7e5dc59b03a2c..aa5b0ece08db7 100644
--- a/docs/user/introduction.asciidoc
+++ b/docs/user/introduction.asciidoc
@@ -20,16 +20,16 @@ and more — all from the convenience of a {kib} UI.
document discovery to SIEM, {kib} is the portal for accessing these and other capabilities.
[role="screenshot"]
-image::images/intro-kibana.png[]
+image::images/intro-kibana.png[Kibana home page]
[float]
[[get-data-into-kibana]]
-=== Add data
+=== Ingest data
-{kib} is designed to use {es} as a data source. Think of Elasticsearch as the engine that stores
+{kib} is designed to use {es} as a data source. Think of {es} as the engine that stores
and processes the data, with {kib} sitting on top.
-From the home page, {kib} provides these options for adding data:
+From the home page, {kib} provides these options for ingesting data:
* Import data using the
https://www.elastic.co/blog/importing-csv-and-log-data-into-elasticsearch-with-file-data-visualizer[File Data visualizer].
@@ -60,7 +60,7 @@ search for hidden insights and relationships. Ask your questions, and then
narrow the results to just the data you want.
[role="screenshot"]
-image::images/intro-discover.png[]
+image::images/intro-discover.png[Discover UI]
[float]
[[visualize-and-analyze]]
@@ -79,7 +79,7 @@ use <> to collect them in one place. A dashboard provides
insights into your data from multiple perspectives.
[role="screenshot"]
-image::images/intro-dashboard.png[]
+image::images/intro-dashboard.png[Sample eCommerce data set dashboard]
{kib} also offers these visualization features:
@@ -156,5 +156,4 @@ You can also <> — no code, no addi
infrastructure required.
Our <> and in-product guidance can
-help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[]
-in the top navigation bar for help with questions or to provide feedback.
+help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[Help icon in navigation bar] for help with questions or to provide feedback.
diff --git a/docs/user/introduction/images/intro-dashboard.png b/docs/user/introduction/images/intro-dashboard.png
index fe4e6f620d19c..bb4e98a516fb7 100644
Binary files a/docs/user/introduction/images/intro-dashboard.png and b/docs/user/introduction/images/intro-dashboard.png differ
diff --git a/docs/user/introduction/images/intro-data-tutorial.png b/docs/user/introduction/images/intro-data-tutorial.png
index 2882a092fbb0b..781e134605b87 100644
Binary files a/docs/user/introduction/images/intro-data-tutorial.png and b/docs/user/introduction/images/intro-data-tutorial.png differ
diff --git a/docs/user/introduction/images/intro-discover.png b/docs/user/introduction/images/intro-discover.png
index 54e5725596421..134804941a356 100644
Binary files a/docs/user/introduction/images/intro-discover.png and b/docs/user/introduction/images/intro-discover.png differ
diff --git a/docs/user/introduction/images/intro-kibana.png b/docs/user/introduction/images/intro-kibana.png
index 62c2c99826131..3d10a31d7e380 100644
Binary files a/docs/user/introduction/images/intro-kibana.png and b/docs/user/introduction/images/intro-kibana.png differ
diff --git a/docs/user/introduction/images/intro-spaces.png b/docs/user/introduction/images/intro-spaces.png
new file mode 100644
index 0000000000000..6f3212cbde26e
Binary files /dev/null and b/docs/user/introduction/images/intro-spaces.png differ
diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc
index 9d735ea1fe3db..047fcc08775e6 100644
--- a/docs/user/monitoring/monitoring-kibana.asciidoc
+++ b/docs/user/monitoring/monitoring-kibana.asciidoc
@@ -48,7 +48,7 @@ By default, if you are running {kib} locally, go to `http://localhost:5601/`.
If {security-features} are enabled, log in.
--
-... Open the menu, then go to *Stack Monitoring*. If data collection is
+... Open the main menu, then click *Stack Monitoring*. If data collection is
disabled, you are prompted to turn it on.
** From the Console or command line, set `xpack.monitoring.collection.enabled`
diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc
index 0c48e3b7d011d..9507b70c4f72e 100644
--- a/docs/user/monitoring/viewing-metrics.asciidoc
+++ b/docs/user/monitoring/viewing-metrics.asciidoc
@@ -80,7 +80,7 @@ By default, if you are running {kib} locally, go to `http://localhost:5601/`.
If the Elastic {security-features} are enabled, log in.
--
-. Open *Stack Monitoring*.
+. Open the main menu, then click *Stack Monitoring*.
+
--
If data collection is disabled, you are prompted to turn on data collection.
diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc
index 371855deb2f3c..413573e7ec182 100644
--- a/docs/user/reporting/automating-report-generation.asciidoc
+++ b/docs/user/reporting/automating-report-generation.asciidoc
@@ -13,7 +13,7 @@ URL that triggers a report to generate.
To create the POST URL for PDF reports:
-. Go to *Dashboard*, then open the visualization or dashboard.
+. Open then main menu, click *Dashboard*, then open a dashboard.
+
To specify a relative or absolute time period, use the time filter.
diff --git a/docs/user/reporting/images/preserve-layout-switch.png b/docs/user/reporting/images/preserve-layout-switch.png
index 9cfbdaafc3ac5..0aaefb14d7ee5 100644
Binary files a/docs/user/reporting/images/preserve-layout-switch.png and b/docs/user/reporting/images/preserve-layout-switch.png differ
diff --git a/docs/user/reporting/images/share-button.png b/docs/user/reporting/images/share-button.png
deleted file mode 100644
index 0b307d947935e..0000000000000
Binary files a/docs/user/reporting/images/share-button.png and /dev/null differ
diff --git a/docs/user/reporting/images/share-menu.png b/docs/user/reporting/images/share-menu.png
new file mode 100644
index 0000000000000..7f1d9eda0b5bc
Binary files /dev/null and b/docs/user/reporting/images/share-menu.png differ
diff --git a/docs/user/reporting/images/shareable-container.png b/docs/user/reporting/images/shareable-container.png
index e114f63e2fe12..829fe15706a52 100644
Binary files a/docs/user/reporting/images/shareable-container.png and b/docs/user/reporting/images/shareable-container.png differ
diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc
index 50ae92382fb24..cd93389bb5fde 100644
--- a/docs/user/reporting/index.asciidoc
+++ b/docs/user/reporting/index.asciidoc
@@ -14,7 +14,7 @@ Reporting is available from the *Share* menu
in *Discover*, *Dashboard*, and *Canvas*.
[role="screenshot"]
-image::user/reporting/images/share-button.png["Share"]
+image::user/reporting/images/share-menu.png["Share"]
[float]
== Setup
@@ -94,7 +94,7 @@ image::user/reporting/images/preserve-layout-switch.png["Share"]
[[manage-report-history]]
== View and manage report history
-For a list of your reports, open the menu, then go to *Stack Management > Alerts and Insights > Reporting*.
+For a list of your reports, open the main menu, then click *Stack Management > Reporting*.
From this view, you can monitor the generation of a report and
download reports that you previously generated.
diff --git a/docs/user/security/api-keys/index.asciidoc b/docs/user/security/api-keys/index.asciidoc
index 7cf1b964082d9..8b59115859622 100644
--- a/docs/user/security/api-keys/index.asciidoc
+++ b/docs/user/security/api-keys/index.asciidoc
@@ -15,7 +15,7 @@ Or, you might create API keys to automate ingestion of new data from
remote sources, without a live user interaction.
You can create API keys from the {kib} Console. To view and invalidate
-API keys, open the menu, then go to *Stack Management > Security > API Keys*.
+API keys, open the main menu, then click *Stack Management > API Keys*.
[role="screenshot"]
image:user/security/api-keys/images/api-keys.png["API Keys UI"]
@@ -39,8 +39,8 @@ or contact your system administrator.
=== Security privileges
You must have the `manage_security`, `manage_api_key`, or the `manage_own_api_key`
-cluster privileges to use API keys in {kib}. To manage roles, open the menu, then go to
-*Stack Management > Security > Roles*, or use the <>.
+cluster privileges to use API keys in {kib}. To manage roles, open the main menu, then click
+*Stack Management > Roles*, or use the <>.
[float]
diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc
index 3af49753db664..150004b3ad691 100644
--- a/docs/user/security/authorization/index.asciidoc
+++ b/docs/user/security/authorization/index.asciidoc
@@ -12,7 +12,7 @@ NOTE: When running multiple tenants of {kib} by changing the `kibana.index` in y
[[xpack-kibana-role-management]]
=== {kib} role management
-To create a role that grants {kib} privileges, open the menu, go to *Stack Management > Security > Roles* and click **Create role**.
+To create a role that grants {kib} privileges, open the main menu, click *Stack Management > Roles*, then click *Create role*.
[[adding_kibana_privileges]]
==== Adding {kib} privileges
diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc
index e1a46a415fe68..b5ab57d8f525a 100644
--- a/docs/user/security/index.asciidoc
+++ b/docs/user/security/index.asciidoc
@@ -13,7 +13,7 @@ auditing. For more information, see
[float]
=== Users
-To create and manage users, open the menu, then go to *Stack Management > Security > Users*.
+To create and manage users, open the main menu, then click *Stack Management > Users*.
You can also change their passwords and roles. For more information about
authentication and built-in users, see
{ref}/setting-up-authentication.html[Setting up user authentication].
@@ -21,7 +21,7 @@ authentication and built-in users, see
[float]
=== Roles
-To manage roles, open the menu, then go to *Stack Management > Security > Roles*, or use
+To manage roles, open the main menu, then click *Stack Management > Roles*, or use
the <>. For more information on configuring roles for {kib}, see <>.
For a more holistic overview of configuring roles for the entire stack,
diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc
index bf7be6284b1a9..2088110f6de21 100644
--- a/docs/user/security/rbac_tutorial.asciidoc
+++ b/docs/user/security/rbac_tutorial.asciidoc
@@ -45,7 +45,7 @@ through in this tutorial:
[float]
==== Create a role
-Open the menu, then go to *Stack Management > Security > Roles*
+Open the main menu, then click *Stack Management > Roles*
for an overview of your roles. This view provides actions
for you to create, edit, and delete roles.
@@ -90,7 +90,7 @@ image::security/images/role-space-visualization.png["Associate space"]
[float]
==== Create the developer user account with the proper roles
-. Open the menu, then go to *Stack Management > Security > Users*.
+. Open the main menu, then click *Stack Management > Users*.
. Click **Create user**, then give the user the `dev-mortgage`
and `monitoring-user` roles, which are required for *Stack Monitoring* users.
diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc
index daf9720a0f1d8..6e7fc0c212f07 100644
--- a/docs/user/security/reporting.asciidoc
+++ b/docs/user/security/reporting.asciidoc
@@ -24,11 +24,11 @@ to report on and the {es} indices.
[[reporting-roles-management-ui]]
=== If you are using the `native` realm
-To assign roles, open the menu, then go to *Stack Management > Security > Roles*, use the <>.
+To assign roles, use the *Roles* UI or <>.
This example shows how to use *Roles* page to create a user who has a custom role and the
`reporting_user` role.
-. Open the menu, then go to *Stack Management > Security > Roles*.
+. Open the main menu, then click *Stack Management > Roles*.
. Click *Create role*, then give the role a name, for example, `custom_reporting_user`.
@@ -51,7 +51,7 @@ that provides read and write privileges in
. Save your new role.
-. Open the menu, then go to *Stack Management > Security > Users*, add a new user, and assign the user the built-in
+. Open the main menu, then click *Stack Management > Users*, add a new user, and assign the user the built-in
`reporting_user` role and your new custom role, `custom_reporting_user`.
[float]
@@ -69,10 +69,10 @@ If you use a different pattern for the `xpack.reporting.index` setting,
you must create a custom role with appropriate access to the index, similar
to the following:
-. Open the menu, then go to *Stack Management >Security > Roles*.
+. Open the main menu, then click *Stack Management > Roles*.
. Click *Create role*, then name the role `custom-reporting-user`.
. Specify the custom index and assign it the `all` index privilege.
-. Open the menu, then go to *Stack Management > Security > Users* and create a new user with
+. Open the main menu, then click *Stack Management > Users* and create a new user with
the `kibana_system` role and the `custom-reporting-user` role.
. Configure {kib} to use the new account:
[source,js]
diff --git a/docs/user/security/role-mappings/index.asciidoc b/docs/user/security/role-mappings/index.asciidoc
index 661c319af827f..3f9a17e98d77f 100644
--- a/docs/user/security/role-mappings/index.asciidoc
+++ b/docs/user/security/role-mappings/index.asciidoc
@@ -9,7 +9,7 @@ or SAML.
Role mappings have no effect for users inside the `native` or `file` realms.
-To manage your role mappings, open the menu, then go to *Stack Management > Security > Role Mappings*.
+To manage your role mappings, open the main menu, then click *Stack Management > Role Mappings*.
With *Role mappings*, you can:
@@ -23,7 +23,7 @@ image:user/security/role-mappings/images/role-mappings-grid.png["Role mappings"]
[float]
=== Create a role mapping
-. Open the menu, then go to *Stack Management > Security > Role Mappings*.
+. Open the main menu, then click *Stack Management > Role Mappings*.
. Click *Create role mapping*.
. Give your role mapping a unique name, and choose which roles you wish to assign to your users.
+
diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc
index e7bd297a3ebb5..613ec88ed0edc 100644
--- a/docs/user/security/securing-kibana.asciidoc
+++ b/docs/user/security/securing-kibana.asciidoc
@@ -81,10 +81,10 @@ use {kib}.
For more information on Basic Authentication and additional methods of
authenticating {kib} users, see <>.
-To manage privileges, open the menu, then go to *Stack Management > Security > Roles*.
+To manage privileges, open the main menu, then click *Stack Management > Roles*.
-If you're using the native realm with Basic Authentication, open then menu,
-then go to *Stack Management > Security > Users* to assign roles, or use the
+If you're using the native realm with Basic Authentication, open then main menu,
+then click *Stack Management > Users* to assign roles, or use the
{ref}/security-api.html#security-user-apis[user management APIs]. For example,
the following creates a user named `jacknich` and assigns it the `kibana_admin`
role:
diff --git a/docs/user/setup.asciidoc b/docs/user/setup.asciidoc
index 31e7d157d1bc7..54bdfff8e0bbb 100644
--- a/docs/user/setup.asciidoc
+++ b/docs/user/setup.asciidoc
@@ -1,5 +1,5 @@
[[setup]]
-= Set up Kibana
+= Set up
[partintro]
--
diff --git a/docs/visualize/images/timelion-copy-expression.png b/docs/visualize/images/timelion-copy-expression.png
deleted file mode 100644
index 376bf7919166e..0000000000000
Binary files a/docs/visualize/images/timelion-copy-expression.png and /dev/null differ
diff --git a/package.json b/package.json
index 84e9c0e2762eb..3a2d13fd5ef3b 100644
--- a/package.json
+++ b/package.json
@@ -228,7 +228,7 @@
"@babel/register": "^7.10.5",
"@babel/types": "^7.11.0",
"@elastic/apm-rum": "^5.6.1",
- "@elastic/charts": "23.2.1",
+ "@elastic/charts": "24.0.0",
"@elastic/ems-client": "7.10.0",
"@elastic/eslint-config-kibana": "0.15.0",
"@elastic/eslint-plugin-eui": "0.0.2",
diff --git a/packages/kbn-monaco/src/esql/constants.ts b/packages/kbn-monaco/src/esql/constants.ts
new file mode 100644
index 0000000000000..59bf9a94d05b2
--- /dev/null
+++ b/packages/kbn-monaco/src/esql/constants.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const ID = 'esql';
diff --git a/packages/kbn-monaco/src/esql/index.ts b/packages/kbn-monaco/src/esql/index.ts
new file mode 100644
index 0000000000000..b0e25af760a26
--- /dev/null
+++ b/packages/kbn-monaco/src/esql/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ID } from './constants';
+import { lexerRules } from './lexer_rules';
+
+export const EsqlLang = { ID, lexerRules };
diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts b/packages/kbn-monaco/src/esql/lexer_rules/esql.ts
similarity index 98%
rename from packages/kbn-monaco/src/xjson/lexer_rules/esql.ts
rename to packages/kbn-monaco/src/esql/lexer_rules/esql.ts
index e75b1013d3727..8badc8ffc4184 100644
--- a/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts
+++ b/packages/kbn-monaco/src/esql/lexer_rules/esql.ts
@@ -17,9 +17,7 @@
* under the License.
*/
-import { monaco } from '../../monaco';
-
-export const ID = 'esql';
+import { monaco } from '../../monaco_imports';
const brackets = [
{ open: '[', close: ']', token: 'delimiter.square' },
diff --git a/packages/kbn-monaco/src/esql/lexer_rules/index.ts b/packages/kbn-monaco/src/esql/lexer_rules/index.ts
new file mode 100644
index 0000000000000..5210bc2411716
--- /dev/null
+++ b/packages/kbn-monaco/src/esql/lexer_rules/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { lexerRules } from './esql';
diff --git a/packages/kbn-monaco/src/index.ts b/packages/kbn-monaco/src/index.ts
index 9213a1bfe1327..2a8467d6ef8fd 100644
--- a/packages/kbn-monaco/src/index.ts
+++ b/packages/kbn-monaco/src/index.ts
@@ -17,8 +17,12 @@
* under the License.
*/
-export { monaco } from './monaco';
+// global setup for supported languages
+import './register_globals';
+
+export { monaco } from './monaco_imports';
export { XJsonLang } from './xjson';
+export { PainlessLang } from './painless';
/* eslint-disable-next-line @kbn/eslint/module_migration */
import * as BarePluginApi from 'monaco-editor/esm/vs/editor/editor.api';
diff --git a/packages/kbn-monaco/src/monaco.ts b/packages/kbn-monaco/src/monaco_imports.ts
similarity index 100%
rename from packages/kbn-monaco/src/monaco.ts
rename to packages/kbn-monaco/src/monaco_imports.ts
diff --git a/packages/kbn-monaco/src/painless/constants.ts b/packages/kbn-monaco/src/painless/constants.ts
new file mode 100644
index 0000000000000..32bbc0aaaa0be
--- /dev/null
+++ b/packages/kbn-monaco/src/painless/constants.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const ID = 'painless';
diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts
new file mode 100644
index 0000000000000..2ff1f4a19f9bd
--- /dev/null
+++ b/packages/kbn-monaco/src/painless/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ID } from './constants';
+import { lexerRules } from './lexer_rules';
+
+export const PainlessLang = { ID, lexerRules };
diff --git a/packages/kbn-monaco/src/painless/lexer_rules/index.ts b/packages/kbn-monaco/src/painless/lexer_rules/index.ts
new file mode 100644
index 0000000000000..7cf9064c6aa51
--- /dev/null
+++ b/packages/kbn-monaco/src/painless/lexer_rules/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { lexerRules } from './painless';
diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts
similarity index 87%
rename from packages/kbn-monaco/src/xjson/lexer_rules/painless.ts
rename to packages/kbn-monaco/src/painless/lexer_rules/painless.ts
index 676eb3134026a..2f4383911c9ad 100644
--- a/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts
+++ b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts
@@ -17,16 +17,9 @@
* under the License.
*/
-import { monaco } from '../../monaco';
+import { monaco } from '../../monaco_imports';
-export const ID = 'painless';
-
-/**
- * Extends the default type for a Monarch language so we can use
- * attribute references (like @keywords to reference the keywords list)
- * in the defined tokenizer
- */
-interface Language extends monaco.languages.IMonarchLanguage {
+export interface Language extends monaco.languages.IMonarchLanguage {
default: string;
brackets: any;
keywords: string[];
@@ -41,8 +34,7 @@ interface Language extends monaco.languages.IMonarchLanguage {
}
export const lexerRules = {
- default: 'invalid',
- tokenPostfix: '',
+ default: '',
// painless does not use < >, so we define our own
brackets: [
['{', '}', 'delimiter.curly'],
@@ -136,9 +128,9 @@ export const lexerRules = {
},
],
// whitespace
- [/[ \t\r\n]+/, { token: 'whitespace' }],
+ [/[ \t\r\n]+/, '@whitespace'],
// comments
- [/\/\*/, 'comment', '@comment'],
+ // [/\/\*/, 'comment', '@comment'],
[/\/\/.*$/, 'comment'],
// brackets
[/[{}()\[\]]/, '@brackets'],
@@ -168,7 +160,6 @@ export const lexerRules = {
// strings single quoted
[/'([^'\\]|\\.)*$/, 'string.invalid'], // string without termination
[/'/, 'string', '@string_sq'],
- [/"""/, { token: 'punctuation.end_triple_quote', nextEmbedded: '@pop' }],
],
comment: [
[/[^\/*]+/, 'comment'],
@@ -189,6 +180,3 @@ export const lexerRules = {
],
},
} as Language;
-
-monaco.languages.register({ id: ID });
-monaco.languages.setMonarchTokensProvider(ID, lexerRules);
diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts
new file mode 100644
index 0000000000000..b9e94803b7542
--- /dev/null
+++ b/packages/kbn-monaco/src/register_globals.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { XJsonLang } from './xjson';
+import { PainlessLang } from './painless';
+import { EsqlLang } from './esql';
+import { monaco } from './monaco_imports';
+// @ts-ignore
+import xJsonWorkerSrc from '!!raw-loader!../target/public/xjson.editor.worker.js';
+// @ts-ignore
+import defaultWorkerSrc from '!!raw-loader!../target/public/default.editor.worker.js';
+
+/**
+ * Register languages and lexer rules
+ */
+monaco.languages.register({ id: XJsonLang.ID });
+monaco.languages.setMonarchTokensProvider(XJsonLang.ID, XJsonLang.lexerRules);
+monaco.languages.setLanguageConfiguration(XJsonLang.ID, XJsonLang.languageConfiguration);
+monaco.languages.register({ id: PainlessLang.ID });
+monaco.languages.setMonarchTokensProvider(PainlessLang.ID, PainlessLang.lexerRules);
+monaco.languages.register({ id: EsqlLang.ID });
+monaco.languages.setMonarchTokensProvider(EsqlLang.ID, EsqlLang.lexerRules);
+
+/**
+ * Create web workers by language ID
+ */
+const mapLanguageIdToWorker: { [key: string]: any } = {
+ [XJsonLang.ID]: xJsonWorkerSrc,
+};
+
+// @ts-ignore
+window.MonacoEnvironment = {
+ getWorker: (module: string, languageId: string) => {
+ const workerSrc = mapLanguageIdToWorker[languageId] || defaultWorkerSrc;
+
+ const blob = new Blob([workerSrc], { type: 'application/javascript' });
+ return new Worker(URL.createObjectURL(blob));
+ },
+};
diff --git a/packages/kbn-monaco/src/xjson/index.ts b/packages/kbn-monaco/src/xjson/index.ts
index 8a4644a3792d2..c372f02c09c76 100644
--- a/packages/kbn-monaco/src/xjson/index.ts
+++ b/packages/kbn-monaco/src/xjson/index.ts
@@ -22,5 +22,6 @@
*/
import './language';
import { ID } from './constants';
+import { lexerRules, languageConfiguration } from './lexer_rules';
-export const XJsonLang = { ID };
+export const XJsonLang = { ID, lexerRules, languageConfiguration };
diff --git a/packages/kbn-monaco/src/xjson/language.ts b/packages/kbn-monaco/src/xjson/language.ts
index 4ae7f2402ed2f..9759dc1b24401 100644
--- a/packages/kbn-monaco/src/xjson/language.ts
+++ b/packages/kbn-monaco/src/xjson/language.ts
@@ -19,32 +19,12 @@
// This file contains a lot of single setup logic for registering a language globally
-import { monaco } from '../monaco';
+import { monaco } from '../monaco_imports';
import { WorkerProxyService } from './worker_proxy_service';
-import { registerLexerRules } from './lexer_rules';
import { ID } from './constants';
-// @ts-ignore
-import workerSrc from '!!raw-loader!../../target/public/xjson.editor.worker.js';
const wps = new WorkerProxyService();
-// Register rules against shared monaco instance.
-registerLexerRules(monaco);
-
-// In future we will need to make this map languages to workers using "id" and/or "label" values
-// that get passed in. Also this should not live inside the "xjson" dir directly. We can update this
-// once we have another worker.
-// @ts-ignore
-window.MonacoEnvironment = {
- getWorker: (module: string, languageId: string) => {
- if (languageId === ID) {
- // In kibana we will probably build this once and then load with raw-loader
- const blob = new Blob([workerSrc], { type: 'application/javascript' });
- return new Worker(URL.createObjectURL(blob));
- }
- },
-};
-
monaco.languages.onLanguage(ID, async () => {
return wps.setup();
});
diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/index.ts b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts
index 515de09510a61..7393c6a68c1bf 100644
--- a/packages/kbn-monaco/src/xjson/lexer_rules/index.ts
+++ b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts
@@ -17,17 +17,4 @@
* under the License.
*/
-/* eslint-disable-next-line @kbn/eslint/module_migration */
-import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
-import * as xJson from './xjson';
-import * as esql from './esql';
-import * as painless from './painless';
-
-export const registerLexerRules = (m: typeof monaco) => {
- m.languages.register({ id: xJson.ID });
- m.languages.setMonarchTokensProvider(xJson.ID, xJson.lexerRules);
- m.languages.register({ id: painless.ID });
- m.languages.setMonarchTokensProvider(painless.ID, painless.lexerRules);
- m.languages.register({ id: esql.ID });
- m.languages.setMonarchTokensProvider(esql.ID, esql.lexerRules);
-};
+export { lexerRules, languageConfiguration } from './xjson';
diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts
index d6fea9e91acfb..e0c566fd3b0f2 100644
--- a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts
+++ b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts
@@ -17,15 +17,10 @@
* under the License.
*/
-import { monaco } from '../../monaco';
-import { ID } from '../constants';
-import './painless';
-import './esql';
+import { monaco } from '../../monaco_imports';
import { globals } from './shared';
-export { ID };
-
export const lexerRules: monaco.languages.IMonarchLanguage = {
...(globals as any),
@@ -124,11 +119,7 @@ export const lexerRules: monaco.languages.IMonarchLanguage = {
},
};
-monaco.languages.register({
- id: ID,
-});
-monaco.languages.setMonarchTokensProvider(ID, lexerRules);
-monaco.languages.setLanguageConfiguration(ID, {
+export const languageConfiguration: monaco.languages.LanguageConfiguration = {
brackets: [
['{', '}'],
['[', ']'],
@@ -138,4 +129,4 @@ monaco.languages.setLanguageConfiguration(ID, {
{ open: '[', close: ']' },
{ open: '"', close: '"' },
],
-});
+};
diff --git a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts
index 548a413a483d9..c0e735b294484 100644
--- a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts
+++ b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts
@@ -18,7 +18,7 @@
*/
import { ParseResult } from './grammar';
-import { monaco } from '../monaco';
+import { monaco } from '../monaco_imports';
import { XJsonWorker } from './worker';
import { ID } from './constants';
diff --git a/packages/kbn-monaco/webpack.config.js b/packages/kbn-monaco/webpack.config.js
index 1a7d8c031670c..53f440689a233 100644
--- a/packages/kbn-monaco/webpack.config.js
+++ b/packages/kbn-monaco/webpack.config.js
@@ -19,33 +19,40 @@
const path = require('path');
-const createLangWorkerConfig = (lang) => ({
- mode: 'production',
- entry: path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`),
- output: {
- path: path.resolve(__dirname, 'target/public'),
- filename: `${lang}.editor.worker.js`,
- },
- resolve: {
- modules: ['node_modules'],
- extensions: ['.js', '.ts', '.tsx'],
- },
- stats: 'errors-only',
- module: {
- rules: [
- {
- test: /\.(js|ts)$/,
- exclude: /node_modules/,
- use: {
- loader: 'babel-loader',
- options: {
- babelrc: false,
- presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
+const createLangWorkerConfig = (lang) => {
+ const entry =
+ lang === 'default'
+ ? 'monaco-editor/esm/vs/editor/editor.worker.js'
+ : path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`);
+
+ return {
+ mode: 'production',
+ entry,
+ output: {
+ path: path.resolve(__dirname, 'target/public'),
+ filename: `${lang}.editor.worker.js`,
+ },
+ resolve: {
+ modules: ['node_modules'],
+ extensions: ['.js', '.ts', '.tsx'],
+ },
+ stats: 'errors-only',
+ module: {
+ rules: [
+ {
+ test: /\.(js|ts)$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ babelrc: false,
+ presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
+ },
},
},
- },
- ],
- },
-});
+ ],
+ },
+ };
+};
-module.exports = [createLangWorkerConfig('xjson')];
+module.exports = [createLangWorkerConfig('xjson'), createLangWorkerConfig('default')];
diff --git a/packages/kbn-plugin-generator/README.md b/packages/kbn-plugin-generator/README.md
index 9ff9a8aa95ca2..bee8e6c2ca783 100644
--- a/packages/kbn-plugin-generator/README.md
+++ b/packages/kbn-plugin-generator/README.md
@@ -51,7 +51,7 @@ yarn kbn bootstrap
Generated plugins receive a handful of scripts that can be used during development. Those scripts are detailed in the [README.md](template/README.md) file in each newly generated plugin, and expose the scripts provided by the [Kibana plugin helpers](../kbn-plugin-helpers), but here is a quick reference in case you need it:
-> ***NOTE:*** All of these scripts should be run from the generated plugin.
+> ***NOTE:*** The following scripts should be run from the generated plugin.
- `yarn kbn bootstrap`
@@ -59,14 +59,6 @@ Generated plugins receive a handful of scripts that can be used during developme
> ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change.
- - `yarn start`
-
- Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana`
-
- ```
- yarn start --elasticsearch.hosts http://localhost:9220
- ```
-
- `yarn build`
Build a distributable archive of your plugin.
@@ -75,4 +67,15 @@ Generated plugins receive a handful of scripts that can be used during developme
Run the server tests using mocha.
+
+To start kibana run the following command from Kibana root.
+
+ - `yarn start`
+
+ Start kibana and it will automatically include this plugin. You can pass any arguments that you would normally send to `bin/kibana`
+
+ ```
+ yarn start --elasticsearch.hosts http://localhost:9220
+ ```
+
For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks run `yarn run` or take a look in the `package.json` file.
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 980d9d02317b6..b1b5d6e2b419e 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -9,7 +9,7 @@
"kbn:watch": "node scripts/build --dev --watch"
},
"dependencies": {
- "@elastic/charts": "23.2.1",
+ "@elastic/charts": "24.0.0",
"@elastic/eui": "29.5.0",
"@elastic/numeral": "^2.5.0",
"@kbn/i18n": "1.0.0",
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
index f5cf6c85fcbef..274d7a4e5a488 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
@@ -138,6 +138,7 @@ kibana_vars=(
tilemap.url
timelion.enabled
vega.enableExternalUrls
+ xpack.actions.proxyUrl
xpack.apm.enabled
xpack.apm.serviceMapEnabled
xpack.apm.ui.enabled
diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx
index 794168132abb2..e9fa2833c3db5 100644
--- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx
+++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx
@@ -22,6 +22,7 @@ import classNames from 'classnames';
import 'brace/theme/textmate';
import 'brace/mode/markdown';
+import 'brace/mode/json';
import {
EuiBadge,
diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts
index 3c4fac81c2c7c..be7836de31246 100644
--- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts
+++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts
@@ -91,6 +91,17 @@ describe('Field', function () {
expect(fieldC.searchable).toEqual(false);
});
+ it('calculates visualizable', () => {
+ const field = getField({ type: 'unknown' });
+ expect(field.visualizable).toEqual(false);
+
+ const fieldB = getField({ type: 'conflict' });
+ expect(fieldB.visualizable).toEqual(false);
+
+ const fieldC = getField({ aggregatable: false, scripted: false });
+ expect(fieldC.visualizable).toEqual(false);
+ });
+
it('calculates aggregatable', () => {
const field = getField({ aggregatable: true, scripted: false });
expect(field.aggregatable).toEqual(true);
diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
index 808afc3449c2a..4a22508f7fef3 100644
--- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
+++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
@@ -18,6 +18,7 @@
*/
import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types';
+import { KBN_FIELD_TYPES } from '../../kbn_field_types/types';
import { IFieldType } from './types';
import { FieldSpec, IndexPattern } from '../..';
@@ -129,7 +130,8 @@ export class IndexPatternField implements IFieldType {
}
public get visualizable() {
- return this.aggregatable;
+ const notVisualizableFieldTypes: string[] = [KBN_FIELD_TYPES.UNKNOWN, KBN_FIELD_TYPES.CONFLICT];
+ return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type);
}
public toJSON() {
diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts
index 57c636a9e3c69..e75b8761984ec 100644
--- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts
+++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { LegacyAPICaller } from 'kibana/server';
+import { ElasticsearchClient } from 'kibana/server';
import { getFieldCapabilities, resolveTimePattern, createNoMatchingIndicesError } from './lib';
@@ -37,10 +37,12 @@ interface FieldSubType {
}
export class IndexPatternsFetcher {
- private _callDataCluster: LegacyAPICaller;
+ private elasticsearchClient: ElasticsearchClient;
+ private allowNoIndices: boolean;
- constructor(callDataCluster: LegacyAPICaller) {
- this._callDataCluster = callDataCluster;
+ constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) {
+ this.elasticsearchClient = elasticsearchClient;
+ this.allowNoIndices = allowNoIndices;
}
/**
@@ -55,10 +57,12 @@ export class IndexPatternsFetcher {
async getFieldsForWildcard(options: {
pattern: string | string[];
metaFields?: string[];
- fieldCapsOptions?: { allowNoIndices: boolean };
+ fieldCapsOptions?: { allow_no_indices: boolean };
}): Promise {
const { pattern, metaFields, fieldCapsOptions } = options;
- return await getFieldCapabilities(this._callDataCluster, pattern, metaFields, fieldCapsOptions);
+ return await getFieldCapabilities(this.elasticsearchClient, pattern, metaFields, {
+ allow_no_indices: fieldCapsOptions ? fieldCapsOptions.allow_no_indices : this.allowNoIndices,
+ });
}
/**
@@ -78,11 +82,11 @@ export class IndexPatternsFetcher {
interval: string;
}) {
const { pattern, lookBack, metaFields } = options;
- const { matches } = await resolveTimePattern(this._callDataCluster, pattern);
+ const { matches } = await resolveTimePattern(this.elasticsearchClient, pattern);
const indices = matches.slice(0, lookBack);
if (indices.length === 0) {
throw createNoMatchingIndicesError(pattern);
}
- return await getFieldCapabilities(this._callDataCluster, indices, metaFields);
+ return await getFieldCapabilities(this.elasticsearchClient, indices, metaFields);
}
}
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js
index 8078ea32187b3..fad20a8f0be06 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js
@@ -32,36 +32,60 @@ describe('server/index_patterns/service/lib/es_api', () => {
afterEach(() => sandbox.restore());
it('calls indices.getAlias() via callCluster', async () => {
- const callCluster = sinon.stub();
+ const getAlias = sinon.stub();
+ const callCluster = {
+ indices: {
+ getAlias,
+ },
+ fieldCaps: sinon.stub(),
+ };
+
await callIndexAliasApi(callCluster);
- sinon.assert.calledOnce(callCluster);
- sinon.assert.calledWith(callCluster, 'indices.getAlias');
+ sinon.assert.calledOnce(getAlias);
});
it('passes indices directly to es api', async () => {
const football = {};
- const callCluster = sinon.stub();
+ const getAlias = sinon.stub();
+ const callCluster = {
+ indices: {
+ getAlias,
+ },
+ fieldCaps: sinon.stub(),
+ };
await callIndexAliasApi(callCluster, football);
- sinon.assert.calledOnce(callCluster);
- expect(callCluster.args[0][1].index).toBe(football);
+ sinon.assert.calledOnce(getAlias);
+ expect(getAlias.args[0][0].index).toBe(football);
});
it('returns the es response directly', async () => {
const football = {};
- const callCluster = sinon.stub().returns(football);
+ const getAlias = sinon.stub().returns(football);
+ const callCluster = {
+ indices: {
+ getAlias,
+ },
+ fieldCaps: sinon.stub(),
+ };
const resp = await callIndexAliasApi(callCluster);
- sinon.assert.calledOnce(callCluster);
+ sinon.assert.calledOnce(getAlias);
expect(resp).toBe(football);
});
it('sets ignoreUnavailable and allowNoIndices params', async () => {
- const callCluster = sinon.stub();
+ const getAlias = sinon.stub();
+ const callCluster = {
+ indices: {
+ getAlias,
+ },
+ fieldCaps: sinon.stub(),
+ };
await callIndexAliasApi(callCluster);
- sinon.assert.calledOnce(callCluster);
+ sinon.assert.calledOnce(getAlias);
- const passedOpts = callCluster.args[0][1];
- expect(passedOpts).toHaveProperty('ignoreUnavailable', true);
- expect(passedOpts).toHaveProperty('allowNoIndices', false);
+ const passedOpts = getAlias.args[0][0];
+ expect(passedOpts).toHaveProperty('ignore_unavailable', true);
+ expect(passedOpts).toHaveProperty('allow_no_indices', false);
});
it('handles errors with convertEsError()', async () => {
@@ -70,9 +94,15 @@ describe('server/index_patterns/service/lib/es_api', () => {
const convertedError = new Error('convertedError');
sandbox.stub(convertEsErrorNS, 'convertEsError').throws(convertedError);
- const callCluster = sinon.spy(async () => {
+ const getAlias = sinon.stub(async () => {
throw esError;
});
+ const callCluster = {
+ indices: {
+ getAlias,
+ },
+ fieldCaps: sinon.stub(),
+ };
try {
await callIndexAliasApi(callCluster, indices);
throw new Error('expected callIndexAliasApi() to throw');
@@ -91,37 +121,60 @@ describe('server/index_patterns/service/lib/es_api', () => {
afterEach(() => sandbox.restore());
it('calls fieldCaps() via callCluster', async () => {
- const callCluster = sinon.stub();
+ const fieldCaps = sinon.stub();
+ const callCluster = {
+ indices: {
+ getAlias: sinon.stub(),
+ },
+ fieldCaps,
+ };
await callFieldCapsApi(callCluster);
- sinon.assert.calledOnce(callCluster);
- sinon.assert.calledWith(callCluster, 'fieldCaps');
+ sinon.assert.calledOnce(fieldCaps);
});
it('passes indices directly to es api', async () => {
const football = {};
- const callCluster = sinon.stub();
+ const fieldCaps = sinon.stub();
+ const callCluster = {
+ indices: {
+ getAlias: sinon.stub(),
+ },
+ fieldCaps,
+ };
await callFieldCapsApi(callCluster, football);
- sinon.assert.calledOnce(callCluster);
- expect(callCluster.args[0][1].index).toBe(football);
+ sinon.assert.calledOnce(fieldCaps);
+ expect(fieldCaps.args[0][0].index).toBe(football);
});
it('returns the es response directly', async () => {
const football = {};
- const callCluster = sinon.stub().returns(football);
+ const fieldCaps = sinon.stub().returns(football);
+ const callCluster = {
+ indices: {
+ getAlias: sinon.stub(),
+ },
+ fieldCaps,
+ };
const resp = await callFieldCapsApi(callCluster);
- sinon.assert.calledOnce(callCluster);
+ sinon.assert.calledOnce(fieldCaps);
expect(resp).toBe(football);
});
it('sets ignoreUnavailable, allowNoIndices, and fields params', async () => {
- const callCluster = sinon.stub();
+ const fieldCaps = sinon.stub();
+ const callCluster = {
+ indices: {
+ getAlias: sinon.stub(),
+ },
+ fieldCaps,
+ };
await callFieldCapsApi(callCluster);
- sinon.assert.calledOnce(callCluster);
+ sinon.assert.calledOnce(fieldCaps);
- const passedOpts = callCluster.args[0][1];
+ const passedOpts = fieldCaps.args[0][0];
expect(passedOpts).toHaveProperty('fields', '*');
- expect(passedOpts).toHaveProperty('ignoreUnavailable', true);
- expect(passedOpts).toHaveProperty('allowNoIndices', false);
+ expect(passedOpts).toHaveProperty('ignore_unavailable', true);
+ expect(passedOpts).toHaveProperty('allow_no_indices', false);
});
it('handles errors with convertEsError()', async () => {
@@ -130,9 +183,15 @@ describe('server/index_patterns/service/lib/es_api', () => {
const convertedError = new Error('convertedError');
sandbox.stub(convertEsErrorNS, 'convertEsError').throws(convertedError);
- const callCluster = sinon.spy(async () => {
+ const fieldCaps = sinon.spy(async () => {
throw esError;
});
+ const callCluster = {
+ indices: {
+ getAlias: sinon.stub(),
+ },
+ fieldCaps,
+ };
try {
await callFieldCapsApi(callCluster, indices);
throw new Error('expected callFieldCapsApi() to throw');
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts
index 27ce14f9a3597..7969324943a9f 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { LegacyAPICaller } from 'kibana/server';
+import { ElasticsearchClient } from 'kibana/server';
import { convertEsError } from './errors';
import { FieldCapsResponse } from './field_capabilities';
@@ -46,15 +46,15 @@ export interface IndexAliasResponse {
* @return {Promise}
*/
export async function callIndexAliasApi(
- callCluster: LegacyAPICaller,
+ callCluster: ElasticsearchClient,
indices: string[] | string
-): Promise {
+) {
try {
- return (await callCluster('indices.getAlias', {
+ return await callCluster.indices.getAlias({
index: indices,
- ignoreUnavailable: true,
- allowNoIndices: false,
- })) as Promise;
+ ignore_unavailable: true,
+ allow_no_indices: false,
+ });
} catch (error) {
throw convertEsError(indices, error);
}
@@ -73,17 +73,17 @@ export async function callIndexAliasApi(
* @return {Promise}
*/
export async function callFieldCapsApi(
- callCluster: LegacyAPICaller,
+ callCluster: ElasticsearchClient,
indices: string[] | string,
- fieldCapsOptions: { allowNoIndices: boolean } = { allowNoIndices: false }
+ fieldCapsOptions: { allow_no_indices: boolean } = { allow_no_indices: false }
) {
try {
- return (await callCluster('fieldCaps', {
+ return await callCluster.fieldCaps({
index: indices,
fields: '*',
- ignoreUnavailable: true,
+ ignore_unavailable: true,
...fieldCapsOptions,
- })) as FieldCapsResponse;
+ });
} catch (error) {
throw convertEsError(indices, error);
}
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js
index 0e5757b7b782b..2d860dc8b1843 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js
@@ -48,9 +48,11 @@ describe('index_patterns/field_capabilities/field_capabilities', () => {
};
const stubDeps = (options = {}) => {
- const { esResponse = {}, fieldsFromFieldCaps = [], mergeOverrides = identity } = options;
+ const { esResponse = [], fieldsFromFieldCaps = [], mergeOverrides = identity } = options;
- sandbox.stub(callFieldCapsApiNS, 'callFieldCapsApi').callsFake(async () => esResponse);
+ sandbox
+ .stub(callFieldCapsApiNS, 'callFieldCapsApi')
+ .callsFake(async () => ({ body: esResponse }));
sandbox.stub(readFieldCapsResponseNS, 'readFieldCapsResponse').returns(fieldsFromFieldCaps);
sandbox.stub(mergeOverridesNS, 'mergeOverrides').callsFake(mergeOverrides);
};
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts
index 62e77e0adad66..b9e3e8aae0899 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts
@@ -19,9 +19,9 @@
import { defaults, keyBy, sortBy } from 'lodash';
-import { LegacyAPICaller } from 'kibana/server';
+import { ElasticsearchClient } from 'kibana/server';
import { callFieldCapsApi } from '../es_api';
-import { FieldCapsResponse, readFieldCapsResponse } from './field_caps_response';
+import { readFieldCapsResponse } from './field_caps_response';
import { mergeOverrides } from './overrides';
import { FieldDescriptor } from '../../index_patterns_fetcher';
@@ -36,17 +36,13 @@ import { FieldDescriptor } from '../../index_patterns_fetcher';
* @return {Promise>}
*/
export async function getFieldCapabilities(
- callCluster: LegacyAPICaller,
+ callCluster: ElasticsearchClient,
indices: string | string[] = [],
metaFields: string[] = [],
- fieldCapsOptions?: { allowNoIndices: boolean }
+ fieldCapsOptions?: { allow_no_indices: boolean }
) {
- const esFieldCaps: FieldCapsResponse = await callFieldCapsApi(
- callCluster,
- indices,
- fieldCapsOptions
- );
- const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name');
+ const esFieldCaps = await callFieldCapsApi(callCluster, indices, fieldCapsOptions);
+ const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps.body), 'name');
const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName)
.filter((name) => !name.startsWith('_'))
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js
index 660e9ec30db6a..87f222aaad89d 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js
@@ -32,6 +32,11 @@ const TIME_PATTERN = '[logs-]dddd-YYYY.w';
describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
let sandbox;
+ const esClientMock = {
+ indices: {
+ getAlias: () => ({}),
+ },
+ };
beforeEach(() => (sandbox = sinon.createSandbox()));
afterEach(() => sandbox.restore());
@@ -39,7 +44,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
describe('pre request', () => {
it('uses callIndexAliasApi() fn', async () => {
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({});
- await resolveTimePattern(noop, TIME_PATTERN);
+ await resolveTimePattern(esClientMock, TIME_PATTERN);
sinon.assert.calledOnce(callIndexAliasApi);
});
@@ -49,7 +54,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard);
- await resolveTimePattern(noop, timePattern);
+ await resolveTimePattern(esClientMock, timePattern);
sinon.assert.calledOnce(timePatternToWildcard);
expect(timePatternToWildcard.firstCall.args).toEqual([timePattern]);
});
@@ -61,7 +66,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({});
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard);
- await resolveTimePattern(noop, timePattern);
+ await resolveTimePattern(esClientMock, timePattern);
sinon.assert.calledOnce(callIndexAliasApi);
expect(callIndexAliasApi.firstCall.args[1]).toBe(wildcard);
});
@@ -70,13 +75,15 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
describe('read response', () => {
it('returns all aliases names in result.all, ordered by time desc', async () => {
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({
- 'logs-2016.2': {},
- 'logs-Saturday-2017.1': {},
- 'logs-2016.1': {},
- 'logs-Sunday-2017.1': {},
- 'logs-2015': {},
- 'logs-2016.3': {},
- 'logs-Friday-2017.1': {},
+ body: {
+ 'logs-2016.2': {},
+ 'logs-Saturday-2017.1': {},
+ 'logs-2016.1': {},
+ 'logs-Sunday-2017.1': {},
+ 'logs-2015': {},
+ 'logs-2016.3': {},
+ 'logs-Friday-2017.1': {},
+ },
});
const resp = await resolveTimePattern(noop, TIME_PATTERN);
@@ -94,13 +101,15 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
it('returns all indices matching the time pattern in matches, ordered by time desc', async () => {
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({
- 'logs-2016.2': {},
- 'logs-Saturday-2017.1': {},
- 'logs-2016.1': {},
- 'logs-Sunday-2017.1': {},
- 'logs-2015': {},
- 'logs-2016.3': {},
- 'logs-Friday-2017.1': {},
+ body: {
+ 'logs-2016.2': {},
+ 'logs-Saturday-2017.1': {},
+ 'logs-2016.1': {},
+ 'logs-Sunday-2017.1': {},
+ 'logs-2015': {},
+ 'logs-2016.3': {},
+ 'logs-Friday-2017.1': {},
+ },
});
const resp = await resolveTimePattern(noop, TIME_PATTERN);
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts
index 2e408d7569be5..95ec06dd9c6e6 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts
@@ -20,7 +20,7 @@
import { chain } from 'lodash';
import moment from 'moment';
-import { LegacyAPICaller } from 'kibana/server';
+import { ElasticsearchClient } from 'kibana/server';
import { timePatternToWildcard } from './time_pattern_to_wildcard';
import { callIndexAliasApi, IndicesAliasResponse } from './es_api';
@@ -36,10 +36,10 @@ import { callIndexAliasApi, IndicesAliasResponse } from './es_api';
* and the indices that actually match the time
* pattern (matches);
*/
-export async function resolveTimePattern(callCluster: LegacyAPICaller, timePattern: string) {
+export async function resolveTimePattern(callCluster: ElasticsearchClient, timePattern: string) {
const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern));
- const allIndexDetails = chain(aliases)
+ const allIndexDetails = chain(aliases.body)
.reduce(
(acc: string[], index: any, indexName: string) =>
acc.concat(indexName, Object.keys(index.aliases || {})),
diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts
index 428e7fef6deea..041eb235d01e0 100644
--- a/src/plugins/data/server/index_patterns/routes.ts
+++ b/src/plugins/data/server/index_patterns/routes.ts
@@ -46,8 +46,8 @@ export function registerRoutes(http: HttpServiceSetup) {
},
},
async (context, request, response) => {
- const { callAsCurrentUser } = context.core.elasticsearch.legacy.client;
- const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser);
+ const { asCurrentUser } = context.core.elasticsearch.client;
+ const indexPatterns = new IndexPatternsFetcher(asCurrentUser);
const { pattern, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
@@ -105,8 +105,8 @@ export function registerRoutes(http: HttpServiceSetup) {
},
},
async (context: RequestHandlerContext, request: any, response: any) => {
- const { callAsCurrentUser } = context.core.elasticsearch.legacy.client;
- const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser);
+ const { asCurrentUser } = context.core.elasticsearch.client;
+ const indexPatterns = new IndexPatternsFetcher(asCurrentUser);
const { pattern, interval, look_back: lookBack, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 3143d5baa5b77..a0cac76cc2cda 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -658,7 +658,7 @@ export const indexPatterns: {
//
// @public (undocumented)
export class IndexPatternsFetcher {
- constructor(callDataCluster: LegacyAPICaller);
+ constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean);
getFieldsForTimePattern(options: {
pattern: string;
metaFields: string[];
@@ -669,7 +669,7 @@ export class IndexPatternsFetcher {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
- allowNoIndices: boolean;
+ allow_no_indices: boolean;
};
}): Promise;
}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx
new file mode 100644
index 0000000000000..2cf626d182eeb
--- /dev/null
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx
@@ -0,0 +1,103 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { findTestSubject } from '@elastic/eui/lib/test';
+// @ts-ignore
+import stubbedLogstashFields from 'fixtures/logstash_fields';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { DiscoverFieldDetails } from './discover_field_details';
+import { coreMock } from '../../../../../../core/public/mocks';
+import { IndexPatternField } from '../../../../../data/public';
+import { getStubIndexPattern } from '../../../../../data/public/test_utils';
+
+const indexPattern = getStubIndexPattern(
+ 'logstash-*',
+ (cfg: any) => cfg,
+ 'time',
+ stubbedLogstashFields(),
+ coreMock.createSetup()
+);
+
+describe('discover sidebar field details', function () {
+ const defaultProps = {
+ indexPattern,
+ details: { buckets: [], error: '', exists: 1, total: true, columns: [] },
+ onAddFilter: jest.fn(),
+ };
+
+ function mountComponent(field: IndexPatternField) {
+ const compProps = { ...defaultProps, field };
+ return mountWithIntl();
+ }
+
+ it('should enable the visualize link for a number field', function () {
+ const visualizableField = new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+ const comp = mountComponent(visualizableField);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy();
+ });
+
+ it('should disable the visualize link for an _id field', function () {
+ const conflictField = new IndexPatternField(
+ {
+ name: '_id',
+ type: 'string',
+ esTypes: ['_id'],
+ count: 0,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'test'
+ );
+ const comp = mountComponent(conflictField);
+ expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({});
+ });
+
+ it('should disable the visualize link for an unknown field', function () {
+ const unknownField = new IndexPatternField(
+ {
+ name: 'test',
+ type: 'unknown',
+ esTypes: ['double'],
+ count: 0,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'test'
+ );
+ const comp = mountComponent(unknownField);
+ expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({});
+ });
+});
diff --git a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
index cdc0f9f0e0451..6b8654f6c3528 100644
--- a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
+++ b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
@@ -64,12 +64,12 @@
.embPanel__titleText {
@include euiTextTruncate;
+ font-weight: $euiFontWeightBold;
}
.embPanel__placeholderTitleText {
- @include euiTextTruncate;
- font-weight: $euiFontWeightRegular;
color: $euiColorMediumShade;
+ font-weight: $euiFontWeightRegular;
}
}
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
index fcf79c1d6b211..c717e4370231e 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { mount } from 'enzyme';
-import { nextTick } from 'test_utils/enzyme_helpers';
+import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { I18nProvider } from '@kbn/i18n/react';
@@ -343,6 +343,88 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
// expect(action.length).toBe(1);
});
+test('Panel title customize link does not exist in view mode', async () => {
+ const inspector = inspectorPluginMock.createStartContract();
+
+ const container = new HelloWorldContainer(
+ { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
+ { getEmbeddableFactory } as any
+ );
+
+ const embeddable = await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE, {
+ firstName: 'Vayon',
+ lastName: 'Poole',
+ });
+
+ const component = mountWithIntl(
+ Promise.resolve([])}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
+ notifications={{} as any}
+ overlays={{} as any}
+ application={applicationMock}
+ inspector={inspector}
+ SavedObjectFinder={() => null}
+ />
+ );
+
+ const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
+ expect(titleLink.length).toBe(0);
+});
+
+test('Runs customize panel action on title click when in edit mode', async () => {
+ const inspector = inspectorPluginMock.createStartContract();
+
+ const container = new HelloWorldContainer(
+ { id: '123', panels: {}, viewMode: ViewMode.EDIT, hidePanelTitles: false },
+ { getEmbeddableFactory } as any
+ );
+
+ const embeddable = await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE, {
+ firstName: 'Vayon',
+ lastName: 'Poole',
+ });
+
+ const component = mountWithIntl(
+ Promise.resolve([])}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
+ notifications={{} as any}
+ overlays={{} as any}
+ application={applicationMock}
+ inspector={inspector}
+ SavedObjectFinder={() => null}
+ />
+ );
+
+ const titleExecute = jest.fn();
+ component.setState((s: any) => ({
+ ...s,
+ universalActions: {
+ ...s.universalActions,
+ customizePanelTitle: { execute: titleExecute, isCompatible: jest.fn() },
+ },
+ }));
+
+ const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
+ expect(titleLink.length).toBe(1);
+ titleLink.simulate('click');
+ await nextTick();
+ expect(titleExecute).toHaveBeenCalledTimes(1);
+});
+
test('Updates when hidePanelTitles is toggled', async () => {
const inspector = inspectorPluginMock.createStartContract();
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
index 137f8c24b1fae..1cd48e85469fd 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
@@ -76,6 +76,7 @@ interface Props {
interface State {
panels: EuiContextMenuPanelDescriptor[];
+ universalActions: PanelUniversalActions;
focusedPanelIndex?: string;
viewMode: ViewMode;
hidePanelTitle: boolean;
@@ -86,6 +87,14 @@ interface State {
error?: EmbeddableError;
}
+interface PanelUniversalActions {
+ customizePanelTitle: CustomizePanelTitleAction;
+ addPanel: AddPanelAction;
+ inspectPanel: InspectPanelAction;
+ removePanel: RemovePanelAction;
+ editPanel: EditPanelAction;
+}
+
export class EmbeddablePanel extends React.Component {
private embeddableRoot: React.RefObject;
private parentSubscription?: Subscription;
@@ -102,6 +111,7 @@ export class EmbeddablePanel extends React.Component {
Boolean(embeddable.getInput()?.hidePanelTitles);
this.state = {
+ universalActions: this.getUniversalActions(),
panels: [],
viewMode,
hidePanelTitle,
@@ -229,6 +239,7 @@ export class EmbeddablePanel extends React.Component {
getActionContextMenuPanel={this.getActionContextMenuPanel}
hidePanelTitle={this.state.hidePanelTitle}
isViewMode={viewOnlyMode}
+ customizeTitle={this.state.universalActions.customizePanelTitle}
closeContextMenu={this.state.closeContextMenu}
title={title}
badges={this.state.badges}
@@ -267,17 +278,7 @@ export class EmbeddablePanel extends React.Component {
}
};
- private getActionContextMenuPanel = async () => {
- let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, {
- embeddable: this.props.embeddable,
- });
-
- const { disabledActions } = this.props.embeddable.getInput();
- if (disabledActions) {
- const removeDisabledActions = removeById(disabledActions);
- regularActions = regularActions.filter(removeDisabledActions);
- }
-
+ private getUniversalActions = (): PanelUniversalActions => {
const createGetUserData = (overlays: OverlayStart) =>
async function getUserData(context: { embeddable: IEmbeddable }) {
return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => {
@@ -299,27 +300,41 @@ export class EmbeddablePanel extends React.Component {
});
};
- // These actions are exposed on the context menu for every embeddable, they bypass the trigger
+ // Universal actions are exposed on the context menu for every embeddable, they bypass the trigger
// registry.
- const extraActions: Array> = [
- new CustomizePanelTitleAction(createGetUserData(this.props.overlays)),
- new AddPanelAction(
+ return {
+ customizePanelTitle: new CustomizePanelTitleAction(createGetUserData(this.props.overlays)),
+ addPanel: new AddPanelAction(
this.props.getEmbeddableFactory,
this.props.getAllEmbeddableFactories,
this.props.overlays,
this.props.notifications,
this.props.SavedObjectFinder
),
- new InspectPanelAction(this.props.inspector),
- new RemovePanelAction(),
- new EditPanelAction(
+ inspectPanel: new InspectPanelAction(this.props.inspector),
+ removePanel: new RemovePanelAction(),
+ editPanel: new EditPanelAction(
this.props.getEmbeddableFactory,
this.props.application,
this.props.stateTransfer
),
- ];
+ };
+ };
- const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField);
+ private getActionContextMenuPanel = async () => {
+ let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, {
+ embeddable: this.props.embeddable,
+ });
+
+ const { disabledActions } = this.props.embeddable.getInput();
+ if (disabledActions) {
+ const removeDisabledActions = removeById(disabledActions);
+ regularActions = regularActions.filter(removeDisabledActions);
+ }
+
+ const sortedActions = [...regularActions, ...Object.values(this.state.universalActions)].sort(
+ sortByOrderField
+ );
return await buildContextMenuForActions({
actions: sortedActions.map((action) => ({
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
index 9bcef051a9359..44f5c3df2709d 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
@@ -24,6 +24,7 @@ import {
EuiToolTip,
EuiScreenReaderOnly,
EuiNotificationBadge,
+ EuiLink,
} from '@elastic/eui';
import classNames from 'classnames';
import React from 'react';
@@ -32,6 +33,7 @@ import { PanelOptionsMenu } from './panel_options_menu';
import { IEmbeddable } from '../../embeddables';
import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers';
import { uiToReactComponent } from '../../../../../kibana_react/public';
+import { CustomizePanelTitleAction } from '.';
export interface PanelHeaderProps {
title?: string;
@@ -44,6 +46,7 @@ export interface PanelHeaderProps {
embeddable: IEmbeddable;
headerId?: string;
showPlaceholderTitle?: boolean;
+ customizeTitle: CustomizePanelTitleAction;
}
function renderBadges(badges: Array>, embeddable: IEmbeddable) {
@@ -129,6 +132,7 @@ export function PanelHeader({
notifications,
embeddable,
headerId,
+ customizeTitle,
}: PanelHeaderProps) {
const description = getViewDescription(embeddable);
const showTitle = !hidePanelTitle && (!isViewMode || title);
@@ -172,11 +176,35 @@ export function PanelHeader({
}
const renderTitle = () => {
- const titleComponent = showTitle ? (
-
- {title || placeholderTitle}
-
- ) : undefined;
+ let titleComponent;
+ if (showTitle) {
+ titleComponent = isViewMode ? (
+
+ {title || placeholderTitle}
+
+ ) : (
+ customizeTitle.execute({ embeddable })}
+ >
+ {title || placeholderTitle}
+
+ );
+ }
return description ? (
requestContext.core.uiSettings.client,
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
index ceae784cf74a6..613f33a47f1f4 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
@@ -50,7 +50,7 @@ describe('AbstractSearchStrategy', () => {
expect(fields).toBe(mockedFields);
expect(req.pre.indexPatternsService.getFieldsForWildcard).toHaveBeenCalledWith({
pattern: indexPattern,
- fieldCapsOptions: { allowNoIndices: true },
+ fieldCapsOptions: { allow_no_indices: true },
});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
index 7b62ad310a354..8b16048f0dce0 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
@@ -86,7 +86,7 @@ export class AbstractSearchStrategy {
return await indexPatternsService!.getFieldsForWildcard({
pattern: indexPattern,
- fieldCapsOptions: { allowNoIndices: true },
+ fieldCapsOptions: { allow_no_indices: true },
});
}
diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts
index cc229ef0c2e08..4a14d43aec249 100644
--- a/test/functional/page_objects/common_page.ts
+++ b/test/functional/page_objects/common_page.ts
@@ -117,11 +117,12 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
} else {
log.debug(`navigateToUrl ${appUrl}`);
await browser.get(appUrl, insertTimestamp);
- // accept alert if it pops up
- const alert = await browser.getAlert();
- await alert?.accept();
}
+ // accept alert if it pops up
+ const alert = await browser.getAlert();
+ await alert?.accept();
+
const currentUrl = shouldLoginIfPrompted
? await this.loginIfPrompted(appUrl, insertTimestamp)
: await browser.getCurrentUrl();
diff --git a/x-pack/package.json b/x-pack/package.json
index d91e11134f9c8..77dc2e662dd28 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -152,7 +152,7 @@
"copy-to-clipboard": "^3.0.8",
"copy-webpack-plugin": "^6.0.2",
"cronstrue": "^1.51.0",
- "cypress": "^5.0.0",
+ "cypress": "5.4.0",
"cypress-multi-reporters": "^1.2.3",
"cypress-promise": "^1.1.0",
"d3": "3.5.17",
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
index 49030dc8cacc5..cf1f4852002ec 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
@@ -5,7 +5,6 @@
*/
import LRU from 'lru-cache';
-import { LegacyAPICaller } from '../../../../../../src/core/server';
import {
IndexPatternsFetcher,
FieldDescriptor,
@@ -45,8 +44,7 @@ export const getDynamicIndexPattern = async ({
}
const indexPatternsFetcher = new IndexPatternsFetcher(
- (...rest: Parameters) =>
- context.core.elasticsearch.legacy.client.callAsCurrentUser(...rest)
+ context.core.elasticsearch.client.asCurrentUser
);
// Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint)
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts
index 374a2420f5ba7..53aa3db00b66a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts
@@ -11,6 +11,16 @@ export enum ApiTokenTypes {
Search = 'search',
}
+export const CREATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.created', {
+ defaultMessage: 'Successfully created key.',
+});
+export const UPDATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.update', {
+ defaultMessage: 'Successfully updated API Key.',
+});
+export const DELETE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.deleted', {
+ defaultMessage: 'Successfully deleted key.',
+});
+
export const SEARCH_DISPLAY = i18n.translate(
'xpack.enterpriseSearch.appSearch.tokens.permissions.display.search',
{
@@ -81,3 +91,5 @@ export const TOKEN_TYPE_INFO = [
{ value: ApiTokenTypes.Private, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Private] },
{ value: ApiTokenTypes.Admin, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Admin] },
];
+
+export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx
index a265b2c998d39..a9a0dab044351 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx
@@ -14,6 +14,7 @@ import { Credentials } from './credentials';
import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui';
import { externalUrl } from '../../../shared/enterprise_search_url';
+import { CredentialsFlyout } from './credentials_flyout';
describe('Credentials', () => {
// Kea mocks
@@ -71,4 +72,16 @@ describe('Credentials', () => {
button.props().onClick();
expect(actions.showCredentialsForm).toHaveBeenCalledTimes(1);
});
+
+ it('will render CredentialsFlyout if shouldShowCredentialsForm is true', () => {
+ setMockValues({ shouldShowCredentialsForm: true });
+ const wrapper = shallow();
+ expect(wrapper.find(CredentialsFlyout)).toHaveLength(1);
+ });
+
+ it('will NOT render CredentialsFlyout if shouldShowCredentialsForm is false', () => {
+ setMockValues({ shouldShowCredentialsForm: false });
+ const wrapper = shallow();
+ expect(wrapper.find(CredentialsFlyout)).toHaveLength(0);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx
index b9a482ae462d5..c8eae8cc13f5f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx
@@ -24,16 +24,19 @@ import {
import { i18n } from '@kbn/i18n';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
+import { FlashMessages } from '../../../shared/flash_messages';
+
import { CredentialsLogic } from './credentials_logic';
import { externalUrl } from '../../../shared/enterprise_search_url/external_url';
import { CredentialsList } from './credentials_list';
+import { CredentialsFlyout } from './credentials_flyout';
export const Credentials: React.FC = () => {
const { initializeCredentialsData, resetCredentials, showCredentialsForm } = useActions(
CredentialsLogic
);
- const { dataLoading } = useValues(CredentialsLogic);
+ const { dataLoading, shouldShowCredentialsForm } = useValues(CredentialsLogic);
useEffect(() => {
initializeCredentialsData();
@@ -63,6 +66,7 @@ export const Credentials: React.FC = () => {
+ {shouldShowCredentialsForm && }
@@ -120,7 +124,8 @@ export const Credentials: React.FC = () => {
)}
-
+
+
{!!dataLoading ? : }
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx
new file mode 100644
index 0000000000000..d2e7ff5f32dd4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiFlyoutBody } from '@elastic/eui';
+
+import { CredentialsFlyoutBody } from './body';
+
+describe('CredentialsFlyoutBody', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+ expect(wrapper.find(EuiFlyoutBody)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx
new file mode 100644
index 0000000000000..2afba633ca892
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiFlyoutBody } from '@elastic/eui';
+
+import { FlashMessages } from '../../../../shared/flash_messages';
+
+export const CredentialsFlyoutBody: React.FC = () => {
+ return (
+
+
+ Details go here
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx
new file mode 100644
index 0000000000000..c31546472b036
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiFlyoutFooter, EuiButtonEmpty } from '@elastic/eui';
+
+import { CredentialsFlyoutFooter } from './footer';
+
+describe('CredentialsFlyoutFooter', () => {
+ const values = {
+ activeApiTokenExists: false,
+ };
+ const actions = {
+ hideCredentialsForm: jest.fn(),
+ onApiTokenChange: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(values);
+ setMockActions(actions);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+ expect(wrapper.find(EuiFlyoutFooter)).toHaveLength(1);
+ });
+
+ it('closes the flyout', () => {
+ const wrapper = shallow();
+ const button = wrapper.find(EuiButtonEmpty);
+ button.simulate('click');
+ expect(button.prop('children')).toEqual('Close');
+ expect(actions.hideCredentialsForm).toHaveBeenCalled();
+ });
+
+ it('renders action button text for new tokens', () => {
+ const wrapper = shallow();
+ const button = wrapper.find('[data-test-subj="APIKeyActionButton"]');
+
+ expect(button.prop('children')).toEqual('Save');
+ });
+
+ it('renders action button text for existing tokens', () => {
+ setMockValues({ activeApiTokenExists: true });
+ const wrapper = shallow();
+ const button = wrapper.find('[data-test-subj="APIKeyActionButton"]');
+
+ expect(button.prop('children')).toEqual('Update');
+ });
+
+ it('calls onApiTokenChange on action button press', () => {
+ const wrapper = shallow();
+ const button = wrapper.find('[data-test-subj="APIKeyActionButton"]');
+ button.simulate('click');
+
+ expect(actions.onApiTokenChange).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx
new file mode 100644
index 0000000000000..e59a75a578ba4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { useValues, useActions } from 'kea';
+import {
+ EuiFlyoutFooter,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { CredentialsLogic } from '../credentials_logic';
+
+export const CredentialsFlyoutFooter: React.FC = () => {
+ const { hideCredentialsForm, onApiTokenChange } = useActions(CredentialsLogic);
+ const { activeApiTokenExists } = useValues(CredentialsLogic);
+
+ return (
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.closeText', {
+ defaultMessage: 'Close',
+ })}
+
+
+
+
+ {activeApiTokenExists
+ ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateText', {
+ defaultMessage: 'Update',
+ })
+ : i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.saveText', {
+ defaultMessage: 'Save',
+ })}
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx
new file mode 100644
index 0000000000000..a8d9505136faa
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { setMockValues } from '../../../../__mocks__/kea.mock';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiFlyoutHeader } from '@elastic/eui';
+
+import { ApiTokenTypes } from '../constants';
+import { IApiToken } from '../types';
+
+import { CredentialsFlyoutHeader } from './header';
+
+describe('CredentialsFlyoutHeader', () => {
+ const apiToken: IApiToken = {
+ name: '',
+ type: ApiTokenTypes.Private,
+ read: true,
+ write: true,
+ access_all_engines: true,
+ };
+ const values = {
+ activeApiToken: apiToken,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(values);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiFlyoutHeader)).toHaveLength(1);
+ expect(wrapper.find('h2').prop('id')).toEqual('credentialsFlyoutTitle');
+ expect(wrapper.find('h2').prop('children')).toEqual('Create a new key');
+ });
+
+ it('changes the title text if editing an existing token', () => {
+ setMockValues({
+ activeApiToken: {
+ ...apiToken,
+ id: 'some-id',
+ name: 'search-key',
+ },
+ });
+ const wrapper = shallow();
+
+ expect(wrapper.find('h2').prop('children')).toEqual('Update search-key');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx
new file mode 100644
index 0000000000000..f208cd1c5918f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { useValues } from 'kea';
+import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { CredentialsLogic } from '../credentials_logic';
+import { FLYOUT_ARIA_LABEL_ID } from '../constants';
+
+export const CredentialsFlyoutHeader: React.FC = () => {
+ const { activeApiToken } = useValues(CredentialsLogic);
+
+ return (
+
+
+
+ {activeApiToken.id
+ ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateTitle', {
+ defaultMessage: 'Update {tokenName}',
+ values: { tokenName: activeApiToken.name },
+ })
+ : i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.createTitle', {
+ defaultMessage: 'Create a new key',
+ })}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx
new file mode 100644
index 0000000000000..16b669c530012
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { setMockActions } from '../../../../__mocks__/kea.mock';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiFlyout } from '@elastic/eui';
+
+import { CredentialsFlyout } from './';
+
+describe('CredentialsFlyout', () => {
+ const actions = {
+ hideCredentialsForm: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(actions);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+ const flyout = wrapper.find(EuiFlyout);
+
+ expect(flyout).toHaveLength(1);
+ expect(flyout.prop('aria-labelledby')).toEqual('credentialsFlyoutTitle');
+ expect(flyout.prop('onClose')).toEqual(actions.hideCredentialsForm);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx
new file mode 100644
index 0000000000000..602a5250716c3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { useActions } from 'kea';
+import { EuiPortal, EuiFlyout } from '@elastic/eui';
+
+import { CredentialsLogic } from '../credentials_logic';
+import { FLYOUT_ARIA_LABEL_ID } from '../constants';
+import { CredentialsFlyoutHeader } from './header';
+import { CredentialsFlyoutBody } from './body';
+import { CredentialsFlyoutFooter } from './footer';
+
+export const CredentialsFlyout: React.FC = () => {
+ const { hideCredentialsForm } = useActions(CredentialsLogic);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts
index 11b1253332cb2..de79862b540ba 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts
@@ -6,17 +6,33 @@
import { resetContext } from 'kea';
-import { CredentialsLogic } from './credentials_logic';
-import { ApiTokenTypes } from './constants';
-
+import { mockHttpValues } from '../../../__mocks__';
jest.mock('../../../shared/http', () => ({
- HttpLogic: { values: { http: { get: jest.fn(), delete: jest.fn() } } },
+ HttpLogic: { values: mockHttpValues },
}));
-import { HttpLogic } from '../../../shared/http';
+const { http } = mockHttpValues;
+
jest.mock('../../../shared/flash_messages', () => ({
+ FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn() } },
+ setSuccessMessage: jest.fn(),
flashAPIErrors: jest.fn(),
}));
-import { flashAPIErrors } from '../../../shared/flash_messages';
+import {
+ FlashMessagesLogic,
+ setSuccessMessage,
+ flashAPIErrors,
+} from '../../../shared/flash_messages';
+
+jest.mock('../../app_logic', () => ({
+ AppLogic: {
+ selectors: { myRole: jest.fn(() => ({})) },
+ values: { myRole: jest.fn(() => ({})) },
+ },
+}));
+import { AppLogic } from '../../app_logic';
+
+import { ApiTokenTypes } from './constants';
+import { CredentialsLogic } from './credentials_logic';
describe('CredentialsLogic', () => {
const DEFAULT_VALUES = {
@@ -38,6 +54,7 @@ describe('CredentialsLogic', () => {
meta: {},
nameInputBlurred: false,
shouldShowCredentialsForm: false,
+ fullEngineAccessChecked: false,
};
const mount = (defaults?: object) => {
@@ -952,6 +969,13 @@ describe('CredentialsLogic', () => {
});
});
});
+
+ describe('listener side-effects', () => {
+ it('should clear flashMessages whenever the credentials form flyout is opened', () => {
+ CredentialsLogic.actions.showCredentialsForm();
+ expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled();
+ });
+ });
});
describe('hideCredentialsForm', () => {
@@ -1068,10 +1092,10 @@ describe('CredentialsLogic', () => {
mount();
jest.spyOn(CredentialsLogic.actions, 'setCredentialsData').mockImplementationOnce(() => {});
const promise = Promise.resolve({ meta, results });
- (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
+ http.get.mockReturnValue(promise);
CredentialsLogic.actions.fetchCredentials(2);
- expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/credentials', {
+ expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials', {
query: {
'page[current]': 2,
},
@@ -1083,7 +1107,7 @@ describe('CredentialsLogic', () => {
it('handles errors', async () => {
mount();
const promise = Promise.reject('An error occured');
- (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
+ http.get.mockReturnValue(promise);
CredentialsLogic.actions.fetchCredentials();
try {
@@ -1101,12 +1125,10 @@ describe('CredentialsLogic', () => {
.spyOn(CredentialsLogic.actions, 'setCredentialsDetails')
.mockImplementationOnce(() => {});
const promise = Promise.resolve(credentialsDetails);
- (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
+ http.get.mockReturnValue(promise);
CredentialsLogic.actions.fetchDetails();
- expect(HttpLogic.values.http.get).toHaveBeenCalledWith(
- '/api/app_search/credentials/details'
- );
+ expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials/details');
await promise;
expect(CredentialsLogic.actions.setCredentialsDetails).toHaveBeenCalledWith(
credentialsDetails
@@ -1116,7 +1138,7 @@ describe('CredentialsLogic', () => {
it('handles errors', async () => {
mount();
const promise = Promise.reject('An error occured');
- (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
+ http.get.mockReturnValue(promise);
CredentialsLogic.actions.fetchDetails();
try {
@@ -1134,20 +1156,19 @@ describe('CredentialsLogic', () => {
mount();
jest.spyOn(CredentialsLogic.actions, 'onApiKeyDelete').mockImplementationOnce(() => {});
const promise = Promise.resolve();
- (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise);
+ http.delete.mockReturnValue(promise);
CredentialsLogic.actions.deleteApiKey(tokenName);
- expect(HttpLogic.values.http.delete).toHaveBeenCalledWith(
- `/api/app_search/credentials/${tokenName}`
- );
+ expect(http.delete).toHaveBeenCalledWith(`/api/app_search/credentials/${tokenName}`);
await promise;
expect(CredentialsLogic.actions.onApiKeyDelete).toHaveBeenCalledWith(tokenName);
+ expect(setSuccessMessage).toHaveBeenCalled();
});
it('handles errors', async () => {
mount();
const promise = Promise.reject('An error occured');
- (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise);
+ http.delete.mockReturnValue(promise);
CredentialsLogic.actions.deleteApiKey(tokenName);
try {
@@ -1157,9 +1178,189 @@ describe('CredentialsLogic', () => {
}
});
});
+
+ describe('onApiTokenChange', () => {
+ it('calls a POST API endpoint that creates a new token if the active token does not exist yet', async () => {
+ const createdToken = {
+ name: 'new-key',
+ type: ApiTokenTypes.Admin,
+ };
+ mount({
+ activeApiToken: createdToken,
+ });
+ jest.spyOn(CredentialsLogic.actions, 'onApiTokenCreateSuccess');
+ const promise = Promise.resolve(createdToken);
+ http.post.mockReturnValue(promise);
+
+ CredentialsLogic.actions.onApiTokenChange();
+ expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', {
+ body: JSON.stringify(createdToken),
+ });
+ await promise;
+ expect(CredentialsLogic.actions.onApiTokenCreateSuccess).toHaveBeenCalledWith(createdToken);
+ expect(setSuccessMessage).toHaveBeenCalled();
+ });
+
+ it('calls a PUT endpoint that updates the active token if it already exists', async () => {
+ const updatedToken = {
+ name: 'test-key',
+ type: ApiTokenTypes.Private,
+ read: true,
+ write: false,
+ access_all_engines: false,
+ engines: ['engine1'],
+ };
+ mount({
+ activeApiToken: {
+ ...updatedToken,
+ id: 'some-id',
+ },
+ });
+ jest.spyOn(CredentialsLogic.actions, 'onApiTokenUpdateSuccess');
+ const promise = Promise.resolve(updatedToken);
+ http.put.mockReturnValue(promise);
+
+ CredentialsLogic.actions.onApiTokenChange();
+ expect(http.put).toHaveBeenCalledWith('/api/app_search/credentials/test-key', {
+ body: JSON.stringify(updatedToken),
+ });
+ await promise;
+ expect(CredentialsLogic.actions.onApiTokenUpdateSuccess).toHaveBeenCalledWith(updatedToken);
+ expect(setSuccessMessage).toHaveBeenCalled();
+ });
+
+ it('handles errors', async () => {
+ mount();
+ const promise = Promise.reject('An error occured');
+ http.post.mockReturnValue(promise);
+
+ CredentialsLogic.actions.onApiTokenChange();
+ try {
+ await promise;
+ } catch {
+ expect(flashAPIErrors).toHaveBeenCalledWith('An error occured');
+ }
+ });
+
+ describe('token type data', () => {
+ it('does not send extra read/write/engine access data for admin tokens', () => {
+ const correctAdminToken = {
+ name: 'bogus-admin',
+ type: ApiTokenTypes.Admin,
+ };
+ const extraData = {
+ read: true,
+ write: true,
+ access_all_engines: true,
+ };
+ mount({ activeApiToken: { ...correctAdminToken, ...extraData } });
+
+ CredentialsLogic.actions.onApiTokenChange();
+ expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', {
+ body: JSON.stringify(correctAdminToken),
+ });
+ });
+
+ it('does not send extra read/write access data for search tokens', () => {
+ const correctSearchToken = {
+ name: 'bogus-search',
+ type: ApiTokenTypes.Search,
+ access_all_engines: false,
+ engines: ['some-engine'],
+ };
+ const extraData = {
+ read: true,
+ write: false,
+ };
+ mount({ activeApiToken: { ...correctSearchToken, ...extraData } });
+
+ CredentialsLogic.actions.onApiTokenChange();
+ expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', {
+ body: JSON.stringify(correctSearchToken),
+ });
+ });
+
+ // Private tokens send all data per the PUT test above.
+ // If that ever changes, we should capture that in another test here.
+ });
+ });
+
+ describe('onEngineSelect', () => {
+ it('calls addEngineName if the engine is not selected', () => {
+ mount({
+ activeApiToken: {
+ ...DEFAULT_VALUES.activeApiToken,
+ engines: [],
+ },
+ });
+ jest.spyOn(CredentialsLogic.actions, 'addEngineName');
+
+ CredentialsLogic.actions.onEngineSelect('engine1');
+ expect(CredentialsLogic.actions.addEngineName).toHaveBeenCalledWith('engine1');
+ expect(CredentialsLogic.values.activeApiToken.engines).toEqual(['engine1']);
+ });
+
+ it('calls removeEngineName if the engine is already selected', () => {
+ mount({
+ activeApiToken: {
+ ...DEFAULT_VALUES.activeApiToken,
+ engines: ['engine1', 'engine2'],
+ },
+ });
+ jest.spyOn(CredentialsLogic.actions, 'removeEngineName');
+
+ CredentialsLogic.actions.onEngineSelect('engine1');
+ expect(CredentialsLogic.actions.removeEngineName).toHaveBeenCalledWith('engine1');
+ expect(CredentialsLogic.values.activeApiToken.engines).toEqual(['engine2']);
+ });
+ });
});
describe('selectors', () => {
+ describe('fullEngineAccessChecked', () => {
+ it('should be true if active token is set to access all engines and the user can access all engines', () => {
+ (AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({
+ canAccessAllEngines: true,
+ });
+ mount({
+ activeApiToken: {
+ ...DEFAULT_VALUES.activeApiToken,
+ access_all_engines: true,
+ },
+ });
+
+ expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(true);
+ });
+
+ it('should be false if the token is not set to access all engines', () => {
+ (AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({
+ canAccessAllEngines: true,
+ });
+ mount({
+ activeApiToken: {
+ ...DEFAULT_VALUES.activeApiToken,
+ access_all_engines: false,
+ },
+ });
+
+ expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(false);
+ });
+
+ it('should be false if the user cannot acess all engines', () => {
+ (AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({
+ canAccessAllEngines: false,
+ });
+ mount({
+ activeApiToken: {
+ ...DEFAULT_VALUES.activeApiToken,
+ access_all_engines: true,
+ },
+ });
+
+ expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(false);
+ });
+ });
+
describe('activeApiTokenExists', () => {
it('should be false if the token has no id', () => {
mount({
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts
index c6f929c45eb23..30b5fabc4d0c4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts
@@ -7,11 +7,17 @@
import { kea, MakeLogicType } from 'kea';
import { formatApiName } from '../../utils/format_api_name';
-import { ApiTokenTypes } from './constants';
+import { ApiTokenTypes, CREATE_MESSAGE, UPDATE_MESSAGE, DELETE_MESSAGE } from './constants';
import { HttpLogic } from '../../../shared/http';
+import {
+ FlashMessagesLogic,
+ setSuccessMessage,
+ flashAPIErrors,
+} from '../../../shared/flash_messages';
+import { AppLogic } from '../../app_logic';
+
import { IMeta } from '../../../../../common/types';
-import { flashAPIErrors } from '../../../shared/flash_messages';
import { IEngine } from '../../types';
import { IApiToken, ICredentialsDetails, ITokenReadWrite } from './types';
@@ -23,9 +29,7 @@ const defaultApiToken: IApiToken = {
access_all_engines: true,
};
-// TODO CREATE_MESSAGE, UPDATE_MESSAGE, and DELETE_MESSAGE from ent-search
-
-export interface ICredentialsLogicActions {
+interface ICredentialsLogicActions {
addEngineName(engineName: string): string;
onApiKeyDelete(tokenName: string): string;
onApiTokenCreateSuccess(apiToken: IApiToken): IApiToken;
@@ -46,9 +50,11 @@ export interface ICredentialsLogicActions {
fetchCredentials(page?: number): number;
fetchDetails(): { value: boolean };
deleteApiKey(tokenName: string): string;
+ onApiTokenChange(): void;
+ onEngineSelect(engineName: string): string;
}
-export interface ICredentialsLogicValues {
+interface ICredentialsLogicValues {
activeApiToken: IApiToken;
activeApiTokenExists: boolean;
activeApiTokenRawName: string;
@@ -79,10 +85,7 @@ export const CredentialsLogic = kea<
setCredentialsData: (meta, apiTokens) => ({ meta, apiTokens }),
setCredentialsDetails: (details) => details,
setNameInputBlurred: (nameInputBlurred) => nameInputBlurred,
- setTokenReadWrite: ({ name, checked }) => ({
- name,
- checked,
- }),
+ setTokenReadWrite: ({ name, checked }) => ({ name, checked }),
setTokenName: (name) => name,
setTokenType: (tokenType) => tokenType,
showCredentialsForm: (apiToken = { ...defaultApiToken }) => apiToken,
@@ -92,6 +95,8 @@ export const CredentialsLogic = kea<
fetchCredentials: (page) => page,
fetchDetails: true,
deleteApiKey: (tokenName) => tokenName,
+ onApiTokenChange: () => null,
+ onEngineSelect: (engineName) => engineName,
}),
reducers: () => ({
apiTokens: [
@@ -204,7 +209,11 @@ export const CredentialsLogic = kea<
],
}),
selectors: ({ selectors }) => ({
- // TODO fullEngineAccessChecked from ent-search
+ fullEngineAccessChecked: [
+ () => [AppLogic.selectors.myRole, selectors.activeApiToken],
+ (myRole, activeApiToken) =>
+ !!(myRole.canAccessAllEngines && activeApiToken.access_all_engines),
+ ],
dataLoading: [
() => [selectors.isCredentialsDetailsComplete, selectors.isCredentialsDataComplete],
(isCredentialsDetailsComplete, isCredentialsDataComplete) => {
@@ -217,6 +226,9 @@ export const CredentialsLogic = kea<
],
}),
listeners: ({ actions, values }) => ({
+ showCredentialsForm: () => {
+ FlashMessagesLogic.actions.clearFlashMessages();
+ },
initializeCredentialsData: () => {
actions.fetchCredentials();
actions.fetchDetails();
@@ -247,11 +259,50 @@ export const CredentialsLogic = kea<
await http.delete(`/api/app_search/credentials/${tokenName}`);
actions.onApiKeyDelete(tokenName);
+ setSuccessMessage(DELETE_MESSAGE);
} catch (e) {
flashAPIErrors(e);
}
},
- // TODO onApiTokenChange from ent-search
- // TODO onEngineSelect from ent-search
+ onApiTokenChange: async () => {
+ const { id, name, engines, type, read, write } = values.activeApiToken;
+
+ const data: IApiToken = {
+ name,
+ type,
+ };
+ if (type === ApiTokenTypes.Private) {
+ data.read = read;
+ data.write = write;
+ }
+ if (type !== ApiTokenTypes.Admin) {
+ data.access_all_engines = values.fullEngineAccessChecked;
+ data.engines = engines;
+ }
+
+ try {
+ const { http } = HttpLogic.values;
+ const body = JSON.stringify(data);
+
+ if (id) {
+ const response = await http.put(`/api/app_search/credentials/${name}`, { body });
+ actions.onApiTokenUpdateSuccess(response);
+ setSuccessMessage(UPDATE_MESSAGE);
+ } else {
+ const response = await http.post('/api/app_search/credentials', { body });
+ actions.onApiTokenCreateSuccess(response);
+ setSuccessMessage(CREATE_MESSAGE);
+ }
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ onEngineSelect: (engineName: string) => {
+ if (values.activeApiToken?.engines?.includes(engineName)) {
+ actions.removeEngineName(engineName);
+ } else {
+ actions.addEngineName(engineName);
+ }
+ },
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx
index f1f16d1a6f7a4..f2bdc1a8c75b5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx
@@ -4,28 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import '../../../__mocks__/kea.mock';
+import { setMockValues } from '../../../__mocks__/kea.mock';
import React from 'react';
-import { useValues } from 'kea';
import { shallow } from 'enzyme';
import { EuiPage } from '@elastic/eui';
-import { ProductSelector } from './';
+import { SetupGuideCta } from '../setup_guide';
import { ProductCard } from '../product_card';
+import { ProductSelector } from './';
+
describe('ProductSelector', () => {
- it('renders the overview page and product cards with no host set', () => {
- (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } }));
+ it('renders the overview page, product cards, & setup guide CTAs with no host set', () => {
+ setMockValues({ config: { host: '' } });
const wrapper = shallow();
expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true);
expect(wrapper.find(ProductCard)).toHaveLength(2);
+ expect(wrapper.find(SetupGuideCta)).toHaveLength(1);
});
describe('access checks when host is set', () => {
beforeEach(() => {
- (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } }));
+ setMockValues({ config: { host: 'localhost' } });
});
it('does not render the App Search card if the user does not have access to AS', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx
index 6d76b741d7a97..235ececd8b6fc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx
@@ -3,11 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
import React from 'react';
import { useValues } from 'kea';
@@ -30,6 +25,7 @@ import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kiba
import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { ProductCard } from '../product_card';
+import { SetupGuideCta } from '../setup_guide';
import AppSearchImage from '../../assets/app_search.png';
import WorkplaceSearchImage from '../../assets/workplace_search.png';
@@ -66,9 +62,13 @@ export const ProductSelector: React.FC = ({ access }) =>
- {i18n.translate('xpack.enterpriseSearch.overview.subheading', {
- defaultMessage: 'Select a product to get started',
- })}
+ {config.host
+ ? i18n.translate('xpack.enterpriseSearch.overview.subheading', {
+ defaultMessage: 'Select a product to get started.',
+ })
+ : i18n.translate('xpack.enterpriseSearch.overview.setupHeading', {
+ defaultMessage: 'Choose a product to set up and get started.',
+ })}
@@ -87,6 +87,7 @@ export const ProductSelector: React.FC = ({ access }) =>
)}
+ {!config.host && }
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts
index c367424d375f9..89f7da4547569 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts
@@ -5,3 +5,4 @@
*/
export { SetupGuide } from './setup_guide';
+export { SetupGuideCta } from './setup_guide_cta';
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss
new file mode 100644
index 0000000000000..103ef8eccb558
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+.enterpriseSearchSetupCta {
+ margin: $euiSize auto $euiSizeXL;
+
+ // Clickable EuiPanel override - line panel up with product cards
+ &.euiPanel--isClickable {
+ width: calc(100% - #{$euiSize});
+ }
+
+ &__text {
+ max-width: $euiSize * 40;
+ }
+
+ &__image {
+ display: block;
+ max-width: 100%;
+ width: $euiSize * 10;
+ margin: 0 auto;
+
+ @include euiBreakpoint('xs', 's') {
+ width: $euiSize * 15;
+ }
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx
new file mode 100644
index 0000000000000..f235beef3b337
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { SetupGuideCta } from './';
+
+describe('SetupGuideCta', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find('.enterpriseSearchSetupCta')).toHaveLength(1);
+ expect(wrapper.find('img')).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx
new file mode 100644
index 0000000000000..2a0e2ffc34f3f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui';
+import { EuiPanel } from '../../../shared/react_router_helpers';
+
+import CtaImage from './assets/getting_started.png';
+import './setup_guide_cta.scss';
+
+export const SetupGuideCta: React.FC = () => (
+
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.overview.setupCta.title', {
+ defaultMessage: 'Enterprise-grade functionality for teams big and small',
+ })}
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.overview.setupCta.description', {
+ defaultMessage:
+ 'Add search to your app or internal organization with Elastic App Search and Workplace Search. Watch the video to see what you can do when search is made easy.',
+ })}
+
+
+
+
+
+
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx
index 803d2c8462b1b..0e929c9191e0f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx
@@ -6,10 +6,8 @@
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPage } from '@elastic/eui';
-import '../__mocks__/kea.mock';
-import { useValues } from 'kea';
+import { setMockValues } from '../__mocks__/kea.mock';
import { EnterpriseSearch } from './';
import { SetupGuide } from './components/setup_guide';
@@ -18,7 +16,7 @@ import { ProductSelector } from './components/product_selector';
describe('EnterpriseSearch', () => {
it('renders the Setup Guide and Product Selector', () => {
- (useValues as jest.Mock).mockReturnValue({
+ setMockValues({
errorConnecting: false,
config: { host: 'localhost' },
});
@@ -28,15 +26,23 @@ describe('EnterpriseSearch', () => {
expect(wrapper.find(ProductSelector)).toHaveLength(1);
});
- it('renders the error connecting prompt when host is not configured', () => {
- (useValues as jest.Mock).mockReturnValueOnce({
+ it('renders the error connecting prompt only if host is configured', () => {
+ setMockValues({
errorConnecting: true,
- config: { host: '' },
+ config: { host: 'localhost' },
});
const wrapper = shallow();
expect(wrapper.find(ErrorConnecting)).toHaveLength(1);
- expect(wrapper.find(EuiPage)).toHaveLength(0);
expect(wrapper.find(ProductSelector)).toHaveLength(0);
+
+ setMockValues({
+ errorConnecting: true,
+ config: { host: '' },
+ });
+ wrapper.setProps({}); // Re-render
+
+ expect(wrapper.find(ErrorConnecting)).toHaveLength(0);
+ expect(wrapper.find(ProductSelector)).toHaveLength(1);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx
index 7b97c6c9e58b6..048baabe6a1dd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx
@@ -25,7 +25,7 @@ export const EnterpriseSearch: React.FC = ({ access = {} }) =>
const { errorConnecting } = useValues(HttpLogic);
const { config } = useValues(KibanaLogic);
- const showErrorConnecting = config.host && errorConnecting;
+ const showErrorConnecting = !!(config.host && errorConnecting);
return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
index 82fbb8940d460..3a4585b6d9a71 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
@@ -8,18 +8,18 @@ import '../../__mocks__/kea.mock';
import React from 'react';
import { shallow, mount } from 'enzyme';
-import { EuiLink, EuiButton } from '@elastic/eui';
+import { EuiLink, EuiButton, EuiPanel } from '@elastic/eui';
import { mockKibanaValues, mockHistory } from '../../__mocks__';
-import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link';
+import { EuiReactRouterLink, EuiReactRouterButton, EuiReactRouterPanel } from './eui_link';
describe('EUI & React Router Component Helpers', () => {
beforeEach(() => {
jest.clearAllMocks();
});
- it('renders', () => {
+ it('renders an EuiLink', () => {
const wrapper = shallow();
expect(wrapper.find(EuiLink)).toHaveLength(1);
@@ -31,6 +31,13 @@ describe('EUI & React Router Component Helpers', () => {
expect(wrapper.find(EuiButton)).toHaveLength(1);
});
+ it('renders an EuiPanel', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiPanel)).toHaveLength(1);
+ expect(wrapper.find(EuiPanel).prop('paddingSize')).toEqual('l');
+ });
+
it('passes down all ...rest props', () => {
const wrapper = shallow();
const link = wrapper.find(EuiLink);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
index f9f6ec54e8832..78546911813ec 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
@@ -6,14 +6,15 @@
import React from 'react';
import { useValues } from 'kea';
-import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui';
+import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps, EuiPanel } from '@elastic/eui';
+import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel';
import { KibanaLogic } from '../kibana';
import { HttpLogic } from '../http';
import { letBrowserHandleEvent, createHref } from './';
/**
- * Generates either an EuiLink or EuiButton with a React-Router-ified link
+ * Generates EUI components with React-Router-ified links
*
* Based off of EUI's recommendations for handling React Router:
* https://github.com/elastic/eui/blob/master/wiki/react-router.md#react-router-51
@@ -54,9 +55,11 @@ export const EuiReactRouterHelper: React.FC = ({
return React.cloneElement(children as React.ReactElement, reactRouterProps);
};
-type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps;
-type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps;
+/**
+ * Component helpers
+ */
+type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps;
export const EuiReactRouterLink: React.FC = ({
to,
onClick,
@@ -68,6 +71,7 @@ export const EuiReactRouterLink: React.FC = ({
);
+type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps;
export const EuiReactRouterButton: React.FC = ({
to,
onClick,
@@ -78,3 +82,15 @@ export const EuiReactRouterButton: React.FC = ({
);
+
+type TEuiReactRouterPanelProps = EuiPanelProps & IEuiReactRouterProps;
+export const EuiReactRouterPanel: React.FC = ({
+ to,
+ onClick,
+ shouldNotCreateHref,
+ ...rest
+}) => (
+
+
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts
index 6915d3222c45c..36fb0560d7323 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts
@@ -6,5 +6,8 @@
export { letBrowserHandleEvent } from './link_events';
export { createHref, ICreateHrefOptions } from './create_href';
-export { EuiReactRouterLink as EuiLink } from './eui_link';
-export { EuiReactRouterButton as EuiButton } from './eui_link';
+export {
+ EuiReactRouterLink as EuiLink,
+ EuiReactRouterButton as EuiButton,
+ EuiReactRouterPanel as EuiPanel,
+} from './eui_link';
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts
index 6b5f4a05b3aa6..357b49de93412 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts
@@ -41,6 +41,115 @@ describe('credentials routes', () => {
});
});
+ describe('POST /api/app_search/credentials', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({ method: 'post', payload: 'body' });
+
+ registerCredentialsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request handler', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/credentials/collection',
+ });
+ });
+
+ describe('validates', () => {
+ describe('admin keys', () => {
+ it('correctly', () => {
+ const request = {
+ body: {
+ name: 'admin-key',
+ type: 'admin',
+ },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('throws on unnecessary properties', () => {
+ const request = {
+ body: {
+ name: 'admin-key',
+ type: 'admin',
+ read: true,
+ access_all_engines: true,
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('private keys', () => {
+ it('correctly', () => {
+ const request = {
+ body: {
+ name: 'private-key',
+ type: 'private',
+ read: true,
+ write: false,
+ access_all_engines: false,
+ engines: ['engine1', 'engine2'],
+ },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('throws on missing keys', () => {
+ const request = {
+ body: {
+ name: 'private-key',
+ type: 'private',
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('search keys', () => {
+ it('correctly', () => {
+ const request = {
+ body: {
+ name: 'search-key',
+ type: 'search',
+ access_all_engines: true,
+ },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('throws on missing keys', () => {
+ const request = {
+ body: {
+ name: 'search-key',
+ type: 'search',
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('throws on extra keys', () => {
+ const request = {
+ body: {
+ name: 'search-key',
+ type: 'search',
+ read: true,
+ write: false,
+ access_all_engines: false,
+ engines: ['engine1', 'engine2'],
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+ });
+ });
+ });
+
describe('GET /api/app_search/credentials/details', () => {
let mockRouter: MockRouter;
@@ -61,6 +170,123 @@ describe('credentials routes', () => {
});
});
+ describe('PUT /api/app_search/credentials/{name}', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({ method: 'put', payload: 'body' });
+
+ registerCredentialsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ const mockRequest = {
+ params: {
+ name: 'abc123',
+ },
+ };
+
+ mockRouter.callRoute(mockRequest);
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/credentials/abc123',
+ });
+ });
+
+ describe('validates', () => {
+ describe('admin keys', () => {
+ it('correctly', () => {
+ const request = {
+ body: {
+ name: 'admin-key',
+ type: 'admin',
+ },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('throws on unnecessary properties', () => {
+ const request = {
+ body: {
+ name: 'admin-key',
+ type: 'admin',
+ read: true,
+ access_all_engines: true,
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('private keys', () => {
+ it('correctly', () => {
+ const request = {
+ body: {
+ name: 'private-key',
+ type: 'private',
+ read: true,
+ write: false,
+ access_all_engines: false,
+ engines: ['engine1', 'engine2'],
+ },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('throws on missing keys', () => {
+ const request = {
+ body: {
+ name: 'private-key',
+ type: 'private',
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('search keys', () => {
+ it('correctly', () => {
+ const request = {
+ body: {
+ name: 'search-key',
+ type: 'search',
+ access_all_engines: true,
+ },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('throws on missing keys', () => {
+ const request = {
+ body: {
+ name: 'search-key',
+ type: 'search',
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('throws on extra keys', () => {
+ const request = {
+ body: {
+ name: 'search-key',
+ type: 'search',
+ read: true,
+ write: false,
+ access_all_engines: false,
+ engines: ['engine1', 'engine2'],
+ },
+ };
+ mockRouter.shouldThrow(request);
+ });
+ });
+ });
+ });
+
describe('DELETE /api/app_search/credentials/{name}', () => {
let mockRouter: MockRouter;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts
index 0f2c1133192c5..85d213c82dd05 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts
@@ -8,10 +8,32 @@ import { schema } from '@kbn/config-schema';
import { IRouteDependencies } from '../../plugin';
+const tokenSchema = schema.oneOf([
+ schema.object({
+ name: schema.string(),
+ type: schema.literal('admin'),
+ }),
+ schema.object({
+ name: schema.string(),
+ type: schema.literal('private'),
+ read: schema.boolean(),
+ write: schema.boolean(),
+ access_all_engines: schema.boolean(),
+ engines: schema.maybe(schema.arrayOf(schema.string())),
+ }),
+ schema.object({
+ name: schema.string(),
+ type: schema.literal('search'),
+ access_all_engines: schema.boolean(),
+ engines: schema.maybe(schema.arrayOf(schema.string())),
+ }),
+]);
+
export function registerCredentialsRoutes({
router,
enterpriseSearchRequestHandler,
}: IRouteDependencies) {
+ // Credentials API
router.get(
{
path: '/api/app_search/credentials',
@@ -25,6 +47,19 @@ export function registerCredentialsRoutes({
path: '/as/credentials/collection',
})
);
+ router.post(
+ {
+ path: '/api/app_search/credentials',
+ validate: {
+ body: tokenSchema,
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/as/credentials/collection',
+ })
+ );
+
+ // TODO: It would be great to remove this someday
router.get(
{
path: '/api/app_search/credentials/details',
@@ -34,6 +69,24 @@ export function registerCredentialsRoutes({
path: '/as/credentials/details',
})
);
+
+ // Single credential API
+ router.put(
+ {
+ path: '/api/app_search/credentials/{name}',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ }),
+ body: tokenSchema,
+ },
+ },
+ async (context, request, response) => {
+ return enterpriseSearchRequestHandler.createRequest({
+ path: `/as/credentials/${request.params.name}`,
+ })(context, request, response);
+ }
+ );
router.delete(
{
path: '/api/app_search/credentials/{name}',
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js
index 5c249ee474b00..6e96ef56d683f 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js
@@ -361,9 +361,10 @@ export class IndexActionsContextMenu extends Component {
diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts
index 2dcab5b49dcdb..2d84e36f3a3ac 100644
--- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts
+++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts
@@ -26,7 +26,6 @@ import {
RequestHandlerContext,
KibanaResponseFactory,
RouteMethod,
- LegacyAPICaller,
} from '../../../../../../../src/core/server';
import { RequestHandler } from '../../../../../../../src/core/server';
import { InfraConfig } from '../../../plugin';
@@ -218,11 +217,7 @@ export class KibanaFramework {
}
public getIndexPatternsService(requestContext: RequestHandlerContext): IndexPatternsFetcher {
- return new IndexPatternsFetcher((...rest: Parameters) => {
- rest[1] = rest[1] || {};
- rest[1].allowNoIndices = true;
- return requestContext.core.elasticsearch.legacy.client.callAsCurrentUser(...rest);
- });
+ return new IndexPatternsFetcher(requestContext.core.elasticsearch.client.asCurrentUser, true);
}
public getSpaceId(request: KibanaRequest): string {
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
index d71b90d16725d..5d39995922b93 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
@@ -18,7 +18,9 @@ interface Props {
}
const Container = styled.div`
- min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize});
+ min-height: calc(
+ 100vh - ${(props) => parseFloat(props.theme.eui.euiHeaderHeightCompensation) * 2}px
+ );
background: ${(props) => props.theme.eui.euiColorEmptyShade};
display: flex;
flex-direction: column;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
index fa9ce24935429..af4c2f78f14a2 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
@@ -22,7 +22,6 @@ import { useCapabilities, useLink } from '../../../../../hooks';
import { useAgentPolicyRefresh } from '../../hooks';
interface InMemoryPackagePolicy extends PackagePolicy {
- inputTypes: string[];
packageName?: string;
packageTitle?: string;
packageVersion?: string;
@@ -56,11 +55,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
// With the package policies provided on input, generate the list of package policies
// used in the InMemoryTable (flattens some values for search) as well as
// the list of options that will be used in the filters dropdowns
- const [packagePolicies, namespaces, inputTypes] = useMemo((): [
- InMemoryPackagePolicy[],
- FilterOption[],
- FilterOption[]
- ] => {
+ const [packagePolicies, namespaces] = useMemo((): [InMemoryPackagePolicy[], FilterOption[]] => {
const namespacesValues: string[] = [];
const inputTypesValues: string[] = [];
const mappedPackagePolicies = originalPackagePolicies.map(
@@ -69,13 +64,8 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
namespacesValues.push(packagePolicy.namespace);
}
- const dsInputTypes: string[] = [];
-
- dsInputTypes.sort(stringSortAscending);
-
return {
...packagePolicy,
- inputTypes: dsInputTypes,
packageName: packagePolicy.package?.name ?? '',
packageTitle: packagePolicy.package?.title ?? '',
packageVersion: packagePolicy.package?.version ?? '',
@@ -86,11 +76,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
namespacesValues.sort(stringSortAscending);
inputTypesValues.sort(stringSortAscending);
- return [
- mappedPackagePolicies,
- namespacesValues.map(toFilterOption),
- inputTypesValues.map(toFilterOption),
- ];
+ return [mappedPackagePolicies, namespacesValues.map(toFilterOption)];
}, [originalPackagePolicies]);
const columns = useMemo(
@@ -273,13 +259,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
name: 'Namespace',
options: namespaces,
multiSelect: 'or',
- },
- {
- type: 'field_value_selection',
- field: 'inputTypes',
- name: 'Input types',
- options: inputTypes,
- multiSelect: 'or',
+ operator: 'exact',
},
],
}}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx
index bb109d766c50a..4e32fa0bbc1b9 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx
@@ -182,7 +182,12 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {
[]
);
- const filterOptions: { [key: string]: string[] } = {
+ const filterOptions: {
+ [key: string]: Array<{
+ value: string;
+ name: string;
+ }>;
+ } = {
dataset: [],
type: [],
namespace: [],
@@ -190,21 +195,37 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {
};
if (dataStreamsData && dataStreamsData.data_streams.length) {
+ const dataValues: {
+ [key: string]: string[];
+ } = {
+ dataset: [],
+ type: [],
+ namespace: [],
+ package: [],
+ };
dataStreamsData.data_streams.forEach((stream) => {
const { dataset, type, namespace, package: pkg } = stream;
- if (!filterOptions.dataset.includes(dataset)) {
- filterOptions.dataset.push(dataset);
+ if (!dataValues.dataset.includes(dataset)) {
+ dataValues.dataset.push(dataset);
}
- if (!filterOptions.type.includes(type)) {
- filterOptions.type.push(type);
+ if (!dataValues.type.includes(type)) {
+ dataValues.type.push(type);
}
- if (!filterOptions.namespace.includes(namespace)) {
- filterOptions.namespace.push(namespace);
+ if (!dataValues.namespace.includes(namespace)) {
+ dataValues.namespace.push(namespace);
}
- if (!filterOptions.package.includes(pkg)) {
- filterOptions.package.push(pkg);
+ if (!dataValues.package.includes(pkg)) {
+ dataValues.package.push(pkg);
}
});
+ for (const field in dataValues) {
+ if (filterOptions[field]) {
+ filterOptions[field] = dataValues[field].sort().map((option) => ({
+ value: option,
+ name: option,
+ }));
+ }
+ }
}
return (
@@ -266,10 +287,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {
defaultMessage: 'Dataset',
}),
multiSelect: 'or',
- options: filterOptions.dataset.map((option) => ({
- value: option,
- name: option,
- })),
+ operator: 'exact',
+ options: filterOptions.dataset,
},
{
type: 'field_value_selection',
@@ -278,10 +297,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {
defaultMessage: 'Type',
}),
multiSelect: 'or',
- options: filterOptions.type.map((option) => ({
- value: option,
- name: option,
- })),
+ operator: 'exact',
+ options: filterOptions.type,
},
{
type: 'field_value_selection',
@@ -290,10 +307,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {
defaultMessage: 'Namespace',
}),
multiSelect: 'or',
- options: filterOptions.namespace.map((option) => ({
- value: option,
- name: option,
- })),
+ operator: 'exact',
+ options: filterOptions.namespace,
},
{
type: 'field_value_selection',
@@ -302,10 +317,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {
defaultMessage: 'Integration',
}),
multiSelect: 'or',
- options: filterOptions.package.map((option) => ({
- value: option,
- name: option,
- })),
+ operator: 'exact',
+ options: filterOptions.package,
},
],
}}
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index e5a06b7e38131..bf5b2aac50643 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -14,6 +14,8 @@ import {
SavedObjectsServiceStart,
HttpServiceSetup,
SavedObjectsClientContract,
+ RequestHandlerContext,
+ KibanaRequest,
} from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
@@ -112,7 +114,11 @@ const allSavedObjectTypes = [
*/
export type ExternalCallback = [
'packagePolicyCreate',
- (newPackagePolicy: NewPackagePolicy) => Promise
+ (
+ newPackagePolicy: NewPackagePolicy,
+ context: RequestHandlerContext,
+ request: KibanaRequest
+ ) => Promise
];
export type ExternalCallbacksStorage = Map>;
diff --git a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts
index db23d6a139f20..44c2ccda3bd2a 100644
--- a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts
@@ -168,45 +168,53 @@ describe('When calling package policy', () => {
const request = getCreateKibanaRequest();
await routeHandler(context, request, response);
expect(response.ok).toHaveBeenCalled();
- expect(callbackOne).toHaveBeenCalledWith({
- policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
- description: '',
- enabled: true,
- inputs: [],
- name: 'endpoint-1',
- namespace: 'default',
- output_id: '',
- package: {
- name: 'endpoint',
- title: 'Elastic Endpoint',
- version: '0.5.0',
+ expect(callbackOne).toHaveBeenCalledWith(
+ {
+ policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
+ description: '',
+ enabled: true,
+ inputs: [],
+ name: 'endpoint-1',
+ namespace: 'default',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.5.0',
+ },
},
- });
- expect(callbackTwo).toHaveBeenCalledWith({
- policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
- description: '',
- enabled: true,
- inputs: [
- {
- type: 'endpoint',
- enabled: true,
- streams: [],
- config: {
- one: {
- value: 'inserted by callbackOne',
+ context,
+ request
+ );
+ expect(callbackTwo).toHaveBeenCalledWith(
+ {
+ policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
+ description: '',
+ enabled: true,
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ one: {
+ value: 'inserted by callbackOne',
+ },
},
},
+ ],
+ name: 'endpoint-1',
+ namespace: 'default',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.5.0',
},
- ],
- name: 'endpoint-1',
- namespace: 'default',
- output_id: '',
- package: {
- name: 'endpoint',
- title: 'Elastic Endpoint',
- version: '0.5.0',
},
- });
+ context,
+ request
+ );
});
it('should create with data from callback', async () => {
diff --git a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts
index 183488265e5af..9e582e6960ade 100644
--- a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts
@@ -90,7 +90,7 @@ export const createPackagePolicyHandler: RequestHandler<
try {
// ensure that the returned value by the callback passes schema validation
updatedNewData = CreatePackagePolicyRequestSchema.body.validate(
- await callback(updatedNewData)
+ await callback(updatedNewData, context, request)
);
} catch (error) {
// Log the error, but keep going and process the other callbacks
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
index 19d69a33788c6..b003d16d379ca 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
@@ -83,7 +83,12 @@ class AgentPolicyService {
return (await this.get(soClient, id)) as AgentPolicy;
}
- public async ensureDefaultAgentPolicy(soClient: SavedObjectsClientContract) {
+ public async ensureDefaultAgentPolicy(
+ soClient: SavedObjectsClientContract
+ ): Promise<{
+ created: boolean;
+ defaultAgentPolicy: AgentPolicy;
+ }> {
const agentPolicies = await soClient.find({
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
searchFields: ['is_default'],
@@ -95,12 +100,18 @@ class AgentPolicyService {
...DEFAULT_AGENT_POLICY,
};
- return this.create(soClient, newDefaultAgentPolicy);
+ return {
+ created: true,
+ defaultAgentPolicy: await this.create(soClient, newDefaultAgentPolicy),
+ };
}
return {
- id: agentPolicies.saved_objects[0].id,
- ...agentPolicies.saved_objects[0].attributes,
+ created: false,
+ defaultAgentPolicy: {
+ id: agentPolicies.saved_objects[0].id,
+ ...agentPolicies.saved_objects[0].attributes,
+ },
};
}
@@ -404,7 +415,9 @@ class AgentPolicyService {
throw new Error('Agent policy not found');
}
- const { id: defaultAgentPolicyId } = await this.ensureDefaultAgentPolicy(soClient);
+ const {
+ defaultAgentPolicy: { id: defaultAgentPolicyId },
+ } = await this.ensureDefaultAgentPolicy(soClient);
if (id === defaultAgentPolicyId) {
throw new Error('The default agent policy cannot be deleted');
}
diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts
index 7f379d3ea4f13..741a23824f010 100644
--- a/x-pack/plugins/ingest_manager/server/services/setup.ts
+++ b/x-pack/plugins/ingest_manager/server/services/setup.ts
@@ -49,7 +49,11 @@ async function createSetupSideEffects(
soClient: SavedObjectsClientContract,
callCluster: CallESAsCurrentUser
): Promise {
- const [installedPackages, defaultOutput, defaultAgentPolicy] = await Promise.all([
+ const [
+ installedPackages,
+ defaultOutput,
+ { created: defaultAgentPolicyCreated, defaultAgentPolicy },
+ ] = await Promise.all([
// packages installed by default
ensureInstalledDefaultPackages(soClient, callCluster),
outputService.ensureDefaultOutput(soClient),
@@ -66,44 +70,46 @@ async function createSetupSideEffects(
}),
]);
- // ensure default packages are added to the default conifg
- const agentPolicyWithPackagePolicies = await agentPolicyService.get(
- soClient,
- defaultAgentPolicy.id,
- true
- );
- if (!agentPolicyWithPackagePolicies) {
- throw new Error('Policy not found');
- }
- if (
- agentPolicyWithPackagePolicies.package_policies.length &&
- typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string'
- ) {
- throw new Error('Policy not found');
- }
-
- for (const installedPackage of installedPackages) {
- const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some(
- (packageName) => installedPackage.name === packageName
+ // If we just created the default policy, ensure default packages are added to it
+ if (defaultAgentPolicyCreated) {
+ const agentPolicyWithPackagePolicies = await agentPolicyService.get(
+ soClient,
+ defaultAgentPolicy.id,
+ true
);
- if (!packageShouldBeInstalled) {
- continue;
+ if (!agentPolicyWithPackagePolicies) {
+ throw new Error('Policy not found');
+ }
+ if (
+ agentPolicyWithPackagePolicies.package_policies.length &&
+ typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string'
+ ) {
+ throw new Error('Policy not found');
}
- const isInstalled = agentPolicyWithPackagePolicies.package_policies.some(
- (d: PackagePolicy | string) => {
- return typeof d !== 'string' && d.package?.name === installedPackage.name;
+ for (const installedPackage of installedPackages) {
+ const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some(
+ (packageName) => installedPackage.name === packageName
+ );
+ if (!packageShouldBeInstalled) {
+ continue;
}
- );
- if (!isInstalled) {
- await addPackageToAgentPolicy(
- soClient,
- callCluster,
- installedPackage,
- agentPolicyWithPackagePolicies,
- defaultOutput
+ const isInstalled = agentPolicyWithPackagePolicies.package_policies.some(
+ (d: PackagePolicy | string) => {
+ return typeof d !== 'string' && d.package?.name === installedPackage.name;
+ }
);
+
+ if (!isInstalled) {
+ await addPackageToAgentPolicy(
+ soClient,
+ callCluster,
+ installedPackage,
+ agentPolicyWithPackagePolicies,
+ defaultOutput
+ );
+ }
}
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
index d91865c21a2a6..3e05d4ddfbc20 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
@@ -28,6 +28,7 @@ import { IBasePath } from '../../../../../../src/core/public';
import { AttributeService } from '../../../../../../src/plugins/embeddable/public';
import { LensAttributeService } from '../../lens_attribute_service';
import { OnSaveProps } from '../../../../../../src/plugins/saved_objects/public/save_modal';
+import { act } from 'react-dom/test-utils';
jest.mock('../../../../../../src/plugins/inspector/public/', () => ({
isAvailable: false,
@@ -337,10 +338,12 @@ describe('embeddable', () => {
} as LensEmbeddableInput);
embeddable.render(mountpoint);
- embeddable.updateInput({
- timeRange,
- query,
- filters: [{ meta: { alias: 'test', negate: true, disabled: true } }],
+ act(() => {
+ embeddable.updateInput({
+ timeRange,
+ query,
+ filters: [{ meta: { alias: 'test', negate: true, disabled: true } }],
+ });
});
expect(expressionRenderer).toHaveBeenCalledTimes(1);
@@ -384,7 +387,9 @@ describe('embeddable', () => {
} as LensEmbeddableInput);
embeddable.render(mountpoint);
- autoRefreshFetchSubject.next();
+ act(() => {
+ autoRefreshFetchSubject.next();
+ });
expect(expressionRenderer).toHaveBeenCalledTimes(2);
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
index d51b8c195c92c..3706611575c6b 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
@@ -312,6 +312,37 @@ describe('xy_visualization', () => {
expect(options.map((o) => o.groupId)).toEqual(['x', 'y', 'breakdown']);
});
+ it('should return the correct labels for the 3 dimensios', () => {
+ const options = xyVisualization.getConfiguration({
+ state: exampleState(),
+ frame,
+ layerId: 'first',
+ }).groups;
+ expect(options.map((o) => o.groupLabel)).toEqual([
+ 'Horizontal axis',
+ 'Vertical axis',
+ 'Break down by',
+ ]);
+ });
+
+ it('should return the correct labels for the 3 dimensios for a horizontal chart', () => {
+ const initialState = exampleState();
+ const state = {
+ ...initialState,
+ layers: [{ ...initialState.layers[0], seriesType: 'bar_horizontal' as SeriesType }],
+ };
+ const options = xyVisualization.getConfiguration({
+ state,
+ frame,
+ layerId: 'first',
+ }).groups;
+ expect(options.map((o) => o.groupLabel)).toEqual([
+ 'Vertical axis',
+ 'Horizontal axis',
+ 'Break down by',
+ ]);
+ });
+
it('should only accept bucketed operations for x', () => {
const options = xyVisualization.getConfiguration({
state: exampleState(),
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
index 76c5a51cb7168..50fbf3f01e34e 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
@@ -156,13 +156,18 @@ export const xyVisualization: Visualization = {
getConfiguration(props) {
const layer = props.state.layers.find((l) => l.layerId === props.layerId)!;
+ const isHorizontal = isHorizontalChart(props.state.layers);
return {
groups: [
{
groupId: 'x',
- groupLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', {
- defaultMessage: 'X-axis',
- }),
+ groupLabel: isHorizontal
+ ? i18n.translate('xpack.lens.xyChart.verticalAxisLabel', {
+ defaultMessage: 'Vertical axis',
+ })
+ : i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', {
+ defaultMessage: 'Horizontal axis',
+ }),
accessors: layer.xAccessor ? [layer.xAccessor] : [],
filterOperations: isBucketed,
suggestedPriority: 1,
@@ -172,9 +177,13 @@ export const xyVisualization: Visualization = {
},
{
groupId: 'y',
- groupLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', {
- defaultMessage: 'Y-axis',
- }),
+ groupLabel: isHorizontal
+ ? i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', {
+ defaultMessage: 'Horizontal axis',
+ })
+ : i18n.translate('xpack.lens.xyChart.verticalAxisLabel', {
+ defaultMessage: 'Vertical axis',
+ }),
accessors: layer.accessors,
filterOperations: isNumericMetric,
supportsMoreColumns: true,
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
index 1f74b0d6d1449..d9b60b670b93e 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
@@ -5,17 +5,21 @@
*/
import { i18n } from '@kbn/i18n';
-import { getNavigateToApp } from '../../../kibana_services';
+import { getCoreOverlays, getNavigateToApp } from '../../../kibana_services';
import { goToSpecifiedPath } from '../../maps_router';
import { getAppTitle } from '../../../../common/i18n_getters';
export const unsavedChangesWarning = i18n.translate(
'xpack.maps.breadCrumbs.unsavedChangesWarning',
{
- defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?',
+ defaultMessage: 'Leave Maps with unsaved work?',
}
);
+export const unsavedChangesTitle = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesTitle', {
+ defaultMessage: 'Unsaved changes',
+});
+
export function getBreadcrumbs({
title,
getHasUnsavedChanges,
@@ -39,10 +43,13 @@ export function getBreadcrumbs({
breadcrumbs.push({
text: getAppTitle(),
- onClick: () => {
+ onClick: async () => {
if (getHasUnsavedChanges()) {
- const navigateAway = window.confirm(unsavedChangesWarning);
- if (navigateAway) {
+ const confirmed = await getCoreOverlays().openConfirm(unsavedChangesWarning, {
+ title: unsavedChangesTitle,
+ 'data-test-subj': 'appLeaveConfirmModal',
+ });
+ if (confirmed) {
goToSpecifiedPath('/');
}
} else {
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
index bd08b2f11fadc..df46d5d6a13ff 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
@@ -43,7 +43,7 @@ import {
import { MapContainer } from '../../../connected_components/map_container';
import { getIndexPatternsFromIds } from '../../../index_pattern_util';
import { getTopNavConfig } from './top_nav_config';
-import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs';
+import { getBreadcrumbs, unsavedChangesTitle, unsavedChangesWarning } from './get_breadcrumbs';
import {
LayerDescriptor,
MapRefreshConfig,
@@ -138,9 +138,7 @@ export class MapsAppView extends React.Component {
this.props.onAppLeave((actions) => {
if (this._hasUnsavedChanges()) {
- if (!window.confirm(unsavedChangesWarning)) {
- return {} as AppLeaveAction;
- }
+ return actions.confirm(unsavedChangesWarning, unsavedChangesTitle);
}
return actions.default() as AppLeaveAction;
});
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js
index 99f5c3eff6984..a1e9a9b4760dd 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js
@@ -54,6 +54,7 @@ describe('ExplorerChart', () => {
);
@@ -79,6 +80,7 @@ describe('ExplorerChart', () => {
seriesConfig={config}
mlSelectSeverityService={mlSelectSeverityServiceMock}
tooltipService={mockTooltipService}
+ severity={0}
/>
);
@@ -111,6 +113,7 @@ describe('ExplorerChart', () => {
seriesConfig={config}
mlSelectSeverityService={mlSelectSeverityServiceMock}
tooltipService={mockTooltipService}
+ severity={0}
/>
);
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js
index 9c04e8187cd30..ee9869a202f58 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js
@@ -34,8 +34,7 @@ import { addItemToRecentlyAccessed } from '../../util/recently_accessed';
const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', {
defaultMessage:
- 'This selection contains too many buckets to be displayed.' +
- 'The dashboard is best viewed over a shorter time range.',
+ 'This selection contains too many buckets to be displayed. You should shorten the time range of the view or narrow the selection in the timeline.',
});
const textViewButton = i18n.translate(
'xpack.ml.explorer.charts.openInSingleMetricViewerButtonLabel',
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
index 12e95e859af53..b9634f0eac359 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
@@ -571,11 +571,14 @@ function calculateChartRange(
let tooManyBuckets = false;
// Calculate the time range for the charts.
// Fit in as many points in the available container width plotted at the job bucket span.
+ // Look for the chart with the shortest bucket span as this determines
+ // the length of the time range that can be plotted.
const midpointMs = Math.ceil((selectedEarliestMs + selectedLatestMs) / 2);
+ const minBucketSpanMs = Math.min.apply(null, map(seriesConfigs, 'bucketSpanSeconds')) * 1000;
const maxBucketSpanMs = Math.max.apply(null, map(seriesConfigs, 'bucketSpanSeconds')) * 1000;
const pointsToPlotFullSelection = Math.ceil(
- (selectedLatestMs - selectedEarliestMs) / maxBucketSpanMs
+ (selectedLatestMs - selectedEarliestMs) / minBucketSpanMs
);
// Optimally space points 5px apart.
@@ -588,16 +591,16 @@ function calculateChartRange(
const halfPoints = Math.ceil(plotPoints / 2);
const timefilter = getTimefilter();
const bounds = timefilter.getActiveBounds();
+ const boundsMin = bounds.min.valueOf();
let chartRange = {
- min: Math.max(midpointMs - halfPoints * maxBucketSpanMs, bounds.min.valueOf()),
- max: Math.min(midpointMs + halfPoints * maxBucketSpanMs, bounds.max.valueOf()),
+ min: Math.max(midpointMs - halfPoints * minBucketSpanMs, boundsMin),
+ max: Math.min(midpointMs + halfPoints * minBucketSpanMs, bounds.max.valueOf()),
};
if (plotPoints > CHART_MAX_POINTS) {
- tooManyBuckets = true;
// For each series being plotted, display the record with the highest score if possible.
- const maxTimeSpan = maxBucketSpanMs * CHART_MAX_POINTS;
+ const maxTimeSpan = minBucketSpanMs * CHART_MAX_POINTS;
let minMs = recordsToPlot[0][timeFieldName];
let maxMs = recordsToPlot[0][timeFieldName];
@@ -620,14 +623,33 @@ function calculateChartRange(
});
if (maxMs - minMs < maxTimeSpan) {
- // Expand out to cover as much as the requested time span as possible.
- minMs = Math.max(selectedEarliestMs, minMs - maxTimeSpan);
- maxMs = Math.min(selectedLatestMs, maxMs + maxTimeSpan);
+ // Expand out before and after the span with the highest scoring anomalies,
+ // covering as much as the requested time span as possible.
+ // Work out if the high scoring region is nearer the start or end of the selected time span.
+ const diff = maxTimeSpan - (maxMs - minMs);
+ if (minMs - 0.5 * diff <= selectedEarliestMs) {
+ minMs = Math.max(selectedEarliestMs, minMs - 0.5 * diff);
+ maxMs = minMs + maxTimeSpan;
+ } else {
+ maxMs = Math.min(selectedLatestMs, maxMs + 0.5 * diff);
+ minMs = maxMs - maxTimeSpan;
+ }
}
chartRange = { min: minMs, max: maxMs };
}
+ // Elasticsearch aggregation returns points at start of bucket,
+ // so align the min to the length of the longest bucket.
+ chartRange.min = Math.floor(chartRange.min / maxBucketSpanMs) * maxBucketSpanMs;
+ if (chartRange.min < boundsMin) {
+ chartRange.min = chartRange.min + maxBucketSpanMs;
+ }
+
+ if (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestMs) {
+ tooManyBuckets = true;
+ }
+
return {
chartRange,
tooManyBuckets,
diff --git a/x-pack/plugins/painless_lab/public/application/components/editor.tsx b/x-pack/plugins/painless_lab/public/application/components/editor.tsx
index b8891ce6524f5..5971c0de5c4ef 100644
--- a/x-pack/plugins/painless_lab/public/application/components/editor.tsx
+++ b/x-pack/plugins/painless_lab/public/application/components/editor.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
+import { PainlessLang } from '@kbn/monaco';
import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public';
interface Props {
@@ -14,7 +15,7 @@ interface Props {
export function Editor({ code, onChange }: Props) {
return (
, so we define our own
- brackets: [
- ['{', '}', 'delimiter.curly'],
- ['[', ']', 'delimiter.square'],
- ['(', ')', 'delimiter.parenthesis'],
- ],
- keywords: [
- 'if',
- 'in',
- 'else',
- 'while',
- 'do',
- 'for',
- 'continue',
- 'break',
- 'return',
- 'new',
- 'try',
- 'catch',
- 'throw',
- 'this',
- 'instanceof',
- ],
- primitives: ['void', 'boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double', 'def'],
- constants: ['true', 'false'],
- operators: [
- '=',
- '>',
- '<',
- '!',
- '~',
- '?',
- '?:',
- '?.',
- ':',
- '==',
- '===',
- '<=',
- '>=',
- '!=',
- '!==',
- '&&',
- '||',
- '++',
- '--',
- '+',
- '-',
- '*',
- '/',
- '&',
- '|',
- '^',
- '%',
- '<<',
- '>>',
- '>>>',
- '+=',
- '-=',
- '*=',
- '/=',
- '&=',
- '|=',
- '^=',
- '%=',
- '<<=',
- '>>=',
- '>>>=',
- '->',
- '::',
- '=~',
- '==~',
- ],
- symbols: /[=> {
@@ -25,8 +24,6 @@ const checkLicenseStatus = (license: ILicense) => {
};
export class PainlessLabUIPlugin implements Plugin {
- languageService = new LanguageService();
-
public setup(
{ http, getStartServices, uiSettings }: CoreSetup,
{ devTools, home, licensing }: PluginDependencies
@@ -80,8 +77,6 @@ export class PainlessLabUIPlugin implements Plugin {
- const blob = new Blob([workerSrc], { type: 'application/javascript' });
- return new Worker(window.URL.createObjectURL(blob));
- },
- };
- }
- }
-
- public stop() {
- if (CAN_CREATE_WORKER) {
- (window as any).MonacoEnvironment = this.originalMonacoEnvironment;
- }
- }
-}
diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts
index 250947d72c5fa..df9907fbf731a 100644
--- a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts
+++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts
@@ -8,6 +8,7 @@ import { keyBy } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Field } from '../../../lib/merge_capabilities_with_fields';
import { RouteDependencies } from '../../../types';
+import type { IndexPatternsFetcher as IndexPatternsFetcherType } from '../../../../../../../src/plugins/data/server';
const parseMetaFields = (metaFields: string | string[]) => {
let parsedFields: string[] = [];
@@ -23,10 +24,10 @@ const getFieldsForWildcardRequest = async (
context: any,
request: any,
response: any,
- IndexPatternsFetcher: any
+ IndexPatternsFetcher: typeof IndexPatternsFetcherType
) => {
- const { callAsCurrentUser } = context.core.elasticsearch.legacy.client;
- const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser);
+ const { asCurrentUser } = context.core.elasticsearch.client;
+ const indexPatterns = new IndexPatternsFetcher(asCurrentUser);
const { pattern, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
index 05000f91f094c..2be9d27b3fecb 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
@@ -278,6 +278,7 @@ export const replaceStateInLocation = ({
replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search))
);
if (history) {
+ newLocation.state = history.location.state;
history.replace(newLocation);
}
return newLocation.search;
diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx
index 589436b945a65..febcf0aee679d 100644
--- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx
+++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx
@@ -11,6 +11,7 @@ import deepEqual from 'fast-deep-equal';
import { SpyRouteProps } from './types';
import { useRouteSpy } from './use_route_spy';
+import { SecurityPageName } from '../../../../common/constants';
export const SpyRouteComponent = memo<
SpyRouteProps & { location: H.Location; pageName: string | undefined }
@@ -50,6 +51,7 @@ export const SpyRouteComponent = memo<
pathName: pathname,
state,
tabName,
+ ...(pageName === SecurityPageName.administration ? { search: search ?? '' } : {}),
},
});
setIsInitializing(false);
diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx
index 9b15007136b2e..e87303efbe526 100644
--- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx
@@ -42,7 +42,7 @@ describe('useUserInfo', () => {
isSignalIndexExists: null,
loading: true,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
},
error: undefined,
});
@@ -53,7 +53,7 @@ describe('useUserInfo', () => {
const spyOnCreateSignalIndex = jest.spyOn(api, 'createSignalIndex');
const spyOnGetSignalIndex = jest.spyOn(api, 'getSignalIndex').mockResolvedValueOnce({
name: 'mock-signal-index',
- template_outdated: true,
+ index_mapping_outdated: true,
});
await act(async () => {
const { waitForNextUpdate } = renderHook(() => useUserInfo(), { wrapper: ManageUserInfo });
diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
index ac2bf438d7fa6..3b0976f459324 100644
--- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
@@ -20,7 +20,7 @@ export interface State {
hasEncryptionKey: boolean | null;
loading: boolean;
signalIndexName: string | null;
- signalIndexTemplateOutdated: boolean | null;
+ signalIndexMappingOutdated: boolean | null;
}
export const initialState: State = {
@@ -32,7 +32,7 @@ export const initialState: State = {
hasEncryptionKey: null,
loading: true,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
};
export type Action =
@@ -66,8 +66,8 @@ export type Action =
signalIndexName: string | null;
}
| {
- type: 'updateSignalIndexTemplateOutdated';
- signalIndexTemplateOutdated: boolean | null;
+ type: 'updateSignalIndexMappingOutdated';
+ signalIndexMappingOutdated: boolean | null;
};
export const userInfoReducer = (state: State, action: Action): State => {
@@ -120,10 +120,10 @@ export const userInfoReducer = (state: State, action: Action): State => {
signalIndexName: action.signalIndexName,
};
}
- case 'updateSignalIndexTemplateOutdated': {
+ case 'updateSignalIndexMappingOutdated': {
return {
...state,
- signalIndexTemplateOutdated: action.signalIndexTemplateOutdated,
+ signalIndexMappingOutdated: action.signalIndexMappingOutdated,
};
}
default:
@@ -156,7 +156,7 @@ export const useUserInfo = (): State => {
hasEncryptionKey,
loading,
signalIndexName,
- signalIndexTemplateOutdated,
+ signalIndexMappingOutdated,
},
dispatch,
] = useUserData();
@@ -171,7 +171,7 @@ export const useUserInfo = (): State => {
loading: indexNameLoading,
signalIndexExists: isApiSignalIndexExists,
signalIndexName: apiSignalIndexName,
- signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated,
+ signalIndexMappingOutdated: apiSignalIndexMappingOutdated,
createDeSignalIndex: createSignalIndex,
} = useSignalIndex();
@@ -234,15 +234,15 @@ export const useUserInfo = (): State => {
useEffect(() => {
if (
!loading &&
- signalIndexTemplateOutdated !== apiSignalIndexTemplateOutdated &&
- apiSignalIndexTemplateOutdated != null
+ signalIndexMappingOutdated !== apiSignalIndexMappingOutdated &&
+ apiSignalIndexMappingOutdated != null
) {
dispatch({
- type: 'updateSignalIndexTemplateOutdated',
- signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated,
+ type: 'updateSignalIndexMappingOutdated',
+ signalIndexMappingOutdated: apiSignalIndexMappingOutdated,
});
}
- }, [dispatch, loading, signalIndexTemplateOutdated, apiSignalIndexTemplateOutdated]);
+ }, [dispatch, loading, signalIndexMappingOutdated, apiSignalIndexMappingOutdated]);
useEffect(() => {
if (
@@ -250,7 +250,7 @@ export const useUserInfo = (): State => {
hasEncryptionKey &&
hasIndexManage &&
((isSignalIndexExists != null && !isSignalIndexExists) ||
- (signalIndexTemplateOutdated != null && signalIndexTemplateOutdated)) &&
+ (signalIndexMappingOutdated != null && signalIndexMappingOutdated)) &&
createSignalIndex != null
) {
createSignalIndex();
@@ -261,7 +261,7 @@ export const useUserInfo = (): State => {
hasEncryptionKey,
isSignalIndexExists,
hasIndexManage,
- signalIndexTemplateOutdated,
+ signalIndexMappingOutdated,
]);
return {
@@ -273,6 +273,6 @@ export const useUserInfo = (): State => {
hasIndexManage,
hasIndexWrite,
signalIndexName,
- signalIndexTemplateOutdated,
+ signalIndexMappingOutdated,
};
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
index 4fd240348f0f3..21b561ec9cddb 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
@@ -980,7 +980,7 @@ export const mockStatusAlertQuery: object = {
export const mockSignalIndex: AlertsIndex = {
name: 'mock-signal-index',
- template_outdated: false,
+ index_mapping_outdated: false,
};
export const mockUserPrivilege: Privilege = {
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
index 59ab416ecc824..dadeb1e7958b5 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
@@ -44,7 +44,7 @@ export interface UpdateAlertStatusProps {
export interface AlertsIndex {
name: string;
- template_outdated: boolean;
+ index_mapping_outdated: boolean;
}
export interface Privilege {
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx
index 1db952526414a..07375a31f3bbc 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx
@@ -26,7 +26,7 @@ describe('useSignalIndex', () => {
loading: true,
signalIndexExists: null,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
});
});
});
@@ -43,7 +43,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: true,
signalIndexName: 'mock-signal-index',
- signalIndexTemplateOutdated: false,
+ signalIndexMappingOutdated: false,
});
});
});
@@ -64,7 +64,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: true,
signalIndexName: 'mock-signal-index',
- signalIndexTemplateOutdated: false,
+ signalIndexMappingOutdated: false,
});
});
});
@@ -104,7 +104,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: false,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
});
});
});
@@ -125,7 +125,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: false,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
index f7d2202736169..1233456359b7f 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
@@ -17,7 +17,7 @@ export interface ReturnSignalIndex {
loading: boolean;
signalIndexExists: boolean | null;
signalIndexName: string | null;
- signalIndexTemplateOutdated: boolean | null;
+ signalIndexMappingOutdated: boolean | null;
createDeSignalIndex: Func | null;
}
@@ -31,7 +31,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
const [signalIndex, setSignalIndex] = useState>({
signalIndexExists: null,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
createDeSignalIndex: null,
});
const [, dispatchToaster] = useStateToaster();
@@ -49,7 +49,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
setSignalIndex({
signalIndexExists: true,
signalIndexName: signal.name,
- signalIndexTemplateOutdated: signal.template_outdated,
+ signalIndexMappingOutdated: signal.index_mapping_outdated,
createDeSignalIndex: createIndex,
});
}
@@ -58,7 +58,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
setSignalIndex({
signalIndexExists: false,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
createDeSignalIndex: createIndex,
});
if (isSecurityAppError(error) && error.body.status_code !== 404) {
@@ -89,7 +89,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
setSignalIndex({
signalIndexExists: false,
signalIndexName: null,
- signalIndexTemplateOutdated: null,
+ signalIndexMappingOutdated: null,
createDeSignalIndex: createIndex,
});
errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster });
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
index 822c57b92b4e5..2d0b9f759f158 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
@@ -45,9 +45,7 @@ export const TrustedAppsPage = memo(() => {
return ;
}
return null;
- // FIXME: Route state is being deleted by some parent component
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [routeState]);
const addButton = (
& {
logger: Logger;
manifestManager?: ManifestManager;
+ appClientFactory: AppClientFactory;
+ security: SecurityPluginSetup;
+ alerts: AlertsPluginStartContract;
+ config: ConfigType;
registerIngestCallback?: IngestManagerStartContract['registerExternalCallback'];
savedObjectsStart: SavedObjectsServiceStart;
};
@@ -93,7 +101,14 @@ export class EndpointAppContextService {
if (this.manifestManager && dependencies.registerIngestCallback) {
dependencies.registerIngestCallback(
'packagePolicyCreate',
- getPackagePolicyCreateCallback(dependencies.logger, this.manifestManager)
+ getPackagePolicyCreateCallback(
+ dependencies.logger,
+ this.manifestManager,
+ dependencies.appClientFactory,
+ dependencies.config.maxTimelineImportExportSize,
+ dependencies.security,
+ dependencies.alerts
+ )
);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
index c28ffcf5b7a3f..1db3e9984284d 100644
--- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { loggingSystemMock } from 'src/core/server/mocks';
+import { httpServerMock, loggingSystemMock } from 'src/core/server/mocks';
import { createNewPackagePolicyMock } from '../../../ingest_manager/common/mocks';
import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
import {
@@ -12,8 +12,23 @@ import {
ManifestManagerMockType,
} from './services/artifacts/manifest_manager/manifest_manager.mock';
import { getPackagePolicyCreateCallback } from './ingest_integration';
+import { KibanaRequest, RequestHandlerContext } from 'kibana/server';
+import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__';
+import { EndpointAppContextServiceStartContract } from './endpoint_app_context_services';
+import { createMockEndpointAppContextServiceStartContract } from './mocks';
describe('ingest_integration tests ', () => {
+ let endpointAppContextMock: EndpointAppContextServiceStartContract;
+ let req: KibanaRequest;
+ let ctx: RequestHandlerContext;
+ const maxTimelineImportExportSize = createMockConfig().maxTimelineImportExportSize;
+
+ beforeEach(() => {
+ endpointAppContextMock = createMockEndpointAppContextServiceStartContract();
+ ctx = requestContextMock.createTools().context;
+ req = httpServerMock.createKibanaRequest();
+ });
+
describe('ingest_integration sanity checks', () => {
test('policy is updated with initial manifest', async () => {
const logger = loggingSystemMock.create().get('ingest_integration.test');
@@ -21,9 +36,16 @@ describe('ingest_integration tests ', () => {
mockType: ManifestManagerMockType.InitialSystemState,
});
- const callback = getPackagePolicyCreateCallback(logger, manifestManager);
+ const callback = getPackagePolicyCreateCallback(
+ logger,
+ manifestManager,
+ endpointAppContextMock.appClientFactory,
+ maxTimelineImportExportSize,
+ endpointAppContextMock.security,
+ endpointAppContextMock.alerts
+ );
const policyConfig = createNewPackagePolicyMock(); // policy config without manifest
- const newPolicyConfig = await callback(policyConfig); // policy config WITH manifest
+ const newPolicyConfig = await callback(policyConfig, ctx, req); // policy config WITH manifest
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
@@ -91,9 +113,16 @@ describe('ingest_integration tests ', () => {
manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error('error updating')]);
const lastComputed = await manifestManager.getLastComputedManifest();
- const callback = getPackagePolicyCreateCallback(logger, manifestManager);
+ const callback = getPackagePolicyCreateCallback(
+ logger,
+ manifestManager,
+ endpointAppContextMock.appClientFactory,
+ maxTimelineImportExportSize,
+ endpointAppContextMock.security,
+ endpointAppContextMock.alerts
+ );
const policyConfig = createNewPackagePolicyMock();
- const newPolicyConfig = await callback(policyConfig);
+ const newPolicyConfig = await callback(policyConfig, ctx, req);
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
@@ -111,9 +140,16 @@ describe('ingest_integration tests ', () => {
expect(lastComputed).toEqual(null);
manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error('abcd'));
- const callback = getPackagePolicyCreateCallback(logger, manifestManager);
+ const callback = getPackagePolicyCreateCallback(
+ logger,
+ manifestManager,
+ endpointAppContextMock.appClientFactory,
+ maxTimelineImportExportSize,
+ endpointAppContextMock.security,
+ endpointAppContextMock.alerts
+ );
const policyConfig = createNewPackagePolicyMock();
- const newPolicyConfig = await callback(policyConfig);
+ const newPolicyConfig = await callback(policyConfig, ctx, req);
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
@@ -125,9 +161,16 @@ describe('ingest_integration tests ', () => {
const lastComputed = await manifestManager.getLastComputedManifest();
manifestManager.buildNewManifest = jest.fn().mockResolvedValue(lastComputed); // no diffs
- const callback = getPackagePolicyCreateCallback(logger, manifestManager);
+ const callback = getPackagePolicyCreateCallback(
+ logger,
+ manifestManager,
+ endpointAppContextMock.appClientFactory,
+ maxTimelineImportExportSize,
+ endpointAppContextMock.security,
+ endpointAppContextMock.alerts
+ );
const policyConfig = createNewPackagePolicyMock();
- const newPolicyConfig = await callback(policyConfig);
+ const newPolicyConfig = await callback(policyConfig, ctx, req);
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
index 489b146daeb43..279603cd621c8 100644
--- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
@@ -4,7 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Logger } from '../../../../../src/core/server';
+import { PluginStartContract as AlertsStartContract } from '../../../alerts/server';
+import { SecurityPluginSetup } from '../../../security/server';
+import { ExternalCallback } from '../../../ingest_manager/server';
+import { KibanaRequest, Logger, RequestHandlerContext } from '../../../../../src/core/server';
import { NewPackagePolicy } from '../../../ingest_manager/common/types/models';
import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
import { NewPolicyData } from '../../common/endpoint/types';
@@ -13,6 +16,10 @@ import { Manifest } from './lib/artifacts';
import { reportErrors } from './lib/artifacts/common';
import { InternalArtifactCompleteSchema } from './schemas/artifacts';
import { manifestDispatchSchema } from '../../common/endpoint/schema/manifest';
+import { AppClientFactory } from '../client';
+import { createDetectionIndex } from '../lib/detection_engine/routes/index/create_index_route';
+import { createPrepackagedRules } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route';
+import { buildFrameworkRequest } from '../lib/timeline/routes/utils/common';
const getManifest = async (logger: Logger, manifestManager: ManifestManager): Promise => {
let manifest: Manifest | null = null;
@@ -71,19 +78,52 @@ const getManifest = async (logger: Logger, manifestManager: ManifestManager): Pr
*/
export const getPackagePolicyCreateCallback = (
logger: Logger,
- manifestManager: ManifestManager
-): ((newPackagePolicy: NewPackagePolicy) => Promise) => {
+ manifestManager: ManifestManager,
+ appClientFactory: AppClientFactory,
+ maxTimelineImportExportSize: number,
+ securitySetup: SecurityPluginSetup,
+ alerts: AlertsStartContract
+): ExternalCallback[1] => {
const handlePackagePolicyCreate = async (
- newPackagePolicy: NewPackagePolicy
+ newPackagePolicy: NewPackagePolicy,
+ context: RequestHandlerContext,
+ request: KibanaRequest
): Promise => {
// We only care about Endpoint package policies
if (newPackagePolicy.package?.name !== 'endpoint') {
return newPackagePolicy;
}
- // We cast the type here so that any changes to the Endpoint specific data
- // follow the types/schema expected
- let updatedPackagePolicy = newPackagePolicy as NewPolicyData;
+ // prep for detection rules creation
+ const appClient = appClientFactory.create(request);
+ const frameworkRequest = await buildFrameworkRequest(context, securitySetup, request);
+
+ // Create detection index & rules (if necessary). move past any failure, this is just a convenience
+ try {
+ await createDetectionIndex(context, appClient);
+ } catch (err) {
+ if (err.statusCode !== 409) {
+ // 409 -> detection index already exists, which is fine
+ logger.warn(
+ `Possible problem creating detection signals index (${err.statusCode}): ${err.message}`
+ );
+ }
+ }
+ try {
+ // this checks to make sure index exists first, safe to try in case of failure above
+ // may be able to recover from minor errors
+ await createPrepackagedRules(
+ context,
+ appClient,
+ alerts.getAlertsClientWithRequest(request),
+ frameworkRequest,
+ maxTimelineImportExportSize
+ );
+ } catch (err) {
+ logger.error(
+ `Unable to create detection rules automatically (${err.statusCode}): ${err.message}`
+ );
+ }
// Get most recent manifest
const manifest = await getManifest(logger, manifestManager);
@@ -94,6 +134,10 @@ export const getPackagePolicyCreateCallback = (
logger.error('Invalid manifest');
}
+ // We cast the type here so that any changes to the Endpoint specific data
+ // follow the types/schema expected
+ let updatedPackagePolicy = newPackagePolicy as NewPolicyData;
+
// Until we get the Default Policy Configuration in the Endpoint package,
// we will add it here manually at creation time.
updatedPackagePolicy = {
diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
index 9fd1fb26b1c58..98b971a00710d 100644
--- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
@@ -6,6 +6,8 @@
import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server';
import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks';
+import { securityMock } from '../../../security/server/mocks';
+import { alertsMock } from '../../../alerts/server/mocks';
import { xpackMocks } from '../../../../mocks';
import {
AgentService,
@@ -14,6 +16,7 @@ import {
PackageService,
} from '../../../ingest_manager/server';
import { createPackagePolicyServiceMock } from '../../../ingest_manager/server/mocks';
+import { AppClientFactory } from '../client';
import { createMockConfig } from '../lib/detection_engine/routes/__mocks__';
import {
EndpointAppContextService,
@@ -57,12 +60,19 @@ export const createMockEndpointAppContextService = (
export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
EndpointAppContextServiceStartContract
> => {
+ const factory = new AppClientFactory();
+ const config = createMockConfig();
+ factory.setup({ getSpaceId: () => 'mockSpace', config });
return {
agentService: createMockAgentService(),
packageService: createMockPackageService(),
logger: loggingSystemMock.create().get('mock_endpoint_app_context'),
savedObjectsStart: savedObjectsServiceMock.createStartContract(),
manifestManager: getManifestManagerMock(),
+ appClientFactory: factory,
+ security: securityMock.createSetup(),
+ alerts: alertsMock.createStart(),
+ config,
registerIngestCallback: jest.fn<
ReturnType,
Parameters
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts
index 473a2dad37f19..e7618f155967b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts
@@ -6,20 +6,19 @@
import { get } from 'lodash';
import { LegacyAPICaller } from '../../../../../../../../src/core/server';
-import { getSignalsTemplate } from './get_signals_template';
import { getTemplateExists } from '../../index/get_template_exists';
+import { SIGNALS_TEMPLATE_VERSION } from './get_signals_template';
export const templateNeedsUpdate = async (callCluster: LegacyAPICaller, index: string) => {
const templateExists = await getTemplateExists(callCluster, index);
- let existingTemplateVersion: number | undefined;
- if (templateExists) {
- const existingTemplate: unknown = await callCluster('indices.getTemplate', {
- name: index,
- });
- existingTemplateVersion = get(existingTemplate, [index, 'version']);
+ if (!templateExists) {
+ return true;
}
- const newTemplate = getSignalsTemplate(index);
- if (existingTemplateVersion === undefined || existingTemplateVersion < newTemplate.version) {
+ const existingTemplate: unknown = await callCluster('indices.getTemplate', {
+ name: index,
+ });
+ const existingTemplateVersion: number | undefined = get(existingTemplate, [index, 'version']);
+ if (existingTemplateVersion === undefined || existingTemplateVersion < SIGNALS_TEMPLATE_VERSION) {
return true;
}
return false;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts
index a801bc18db439..287459cf5ec9a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts
@@ -4,17 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IRouter } from '../../../../../../../../src/core/server';
+import { AppClient } from '../../../../types';
+import { IRouter, RequestHandlerContext } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
import { transformError, buildSiemResponse } from '../utils';
import { getIndexExists } from '../../index/get_index_exists';
import { getPolicyExists } from '../../index/get_policy_exists';
import { setPolicy } from '../../index/set_policy';
import { setTemplate } from '../../index/set_template';
-import { getSignalsTemplate } from './get_signals_template';
+import { getSignalsTemplate, SIGNALS_TEMPLATE_VERSION } from './get_signals_template';
import { createBootstrapIndex } from '../../index/create_bootstrap_index';
import signalsPolicy from './signals_policy.json';
import { templateNeedsUpdate } from './check_template_version';
+import { getIndexVersion } from './get_index_version';
export const createIndexRoute = (router: IRouter) => {
router.post(
@@ -29,29 +31,11 @@ export const createIndexRoute = (router: IRouter) => {
const siemResponse = buildSiemResponse(response);
try {
- const clusterClient = context.core.elasticsearch.legacy.client;
const siemClient = context.securitySolution?.getAppClient();
- const callCluster = clusterClient.callAsCurrentUser;
-
if (!siemClient) {
return siemResponse.error({ statusCode: 404 });
}
-
- const index = siemClient.getSignalsIndex();
- const indexExists = await getIndexExists(callCluster, index);
- if (await templateNeedsUpdate(callCluster, index)) {
- const policyExists = await getPolicyExists(callCluster, index);
- if (!policyExists) {
- await setPolicy(callCluster, index, signalsPolicy);
- }
- await setTemplate(callCluster, index, getSignalsTemplate(index));
- if (indexExists) {
- await callCluster('indices.rollover', { alias: index });
- }
- }
- if (!indexExists) {
- await createBootstrapIndex(callCluster, index);
- }
+ await createDetectionIndex(context, siemClient!);
return response.ok({ body: { acknowledged: true } });
} catch (err) {
const error = transformError(err);
@@ -63,3 +47,41 @@ export const createIndexRoute = (router: IRouter) => {
}
);
};
+
+class CreateIndexError extends Error {
+ public readonly statusCode: number;
+ constructor(message: string, statusCode: number) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+}
+
+export const createDetectionIndex = async (
+ context: RequestHandlerContext,
+ siemClient: AppClient
+): Promise => {
+ const clusterClient = context.core.elasticsearch.legacy.client;
+ const callCluster = clusterClient.callAsCurrentUser;
+
+ if (!siemClient) {
+ throw new CreateIndexError('', 404);
+ }
+
+ const index = siemClient.getSignalsIndex();
+ const policyExists = await getPolicyExists(callCluster, index);
+ if (!policyExists) {
+ await setPolicy(callCluster, index, signalsPolicy);
+ }
+ if (await templateNeedsUpdate(callCluster, index)) {
+ await setTemplate(callCluster, index, getSignalsTemplate(index));
+ }
+ const indexExists = await getIndexExists(callCluster, index);
+ if (indexExists) {
+ const indexVersion = await getIndexVersion(callCluster, index);
+ if (indexVersion !== SIGNALS_TEMPLATE_VERSION) {
+ await callCluster('indices.rollover', { alias: index });
+ }
+ } else {
+ await createBootstrapIndex(callCluster, index);
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts
new file mode 100644
index 0000000000000..062cffd393555
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get } from 'lodash';
+import { LegacyAPICaller } from '../../../../../../../../src/core/server';
+import { readIndex } from '../../index/read_index';
+
+interface IndicesAliasResponse {
+ [index: string]: IndexAliasResponse;
+}
+
+interface IndexAliasResponse {
+ aliases: {
+ [aliasName: string]: Record;
+ };
+}
+
+export const getIndexVersion = async (
+ callCluster: LegacyAPICaller,
+ index: string
+): Promise => {
+ const indexAlias: IndicesAliasResponse = await callCluster('indices.getAlias', {
+ index,
+ });
+ const writeIndex = Object.keys(indexAlias).find(
+ (key) => indexAlias[key].aliases[index].is_write_index
+ );
+ if (writeIndex === undefined) {
+ return undefined;
+ }
+ const writeIndexMapping = await readIndex(callCluster, writeIndex);
+ return get(writeIndexMapping, [writeIndex, 'mappings', '_meta', 'version']);
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts
index b676ab5705bfc..d1a9b701b2c9d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts
@@ -7,8 +7,10 @@
import signalsMapping from './signals_mapping.json';
import ecsMapping from './ecs_mapping.json';
+export const SIGNALS_TEMPLATE_VERSION = 2;
+export const MIN_EQL_RULE_INDEX_VERSION = 2;
+
export const getSignalsTemplate = (index: string) => {
- const version = 2;
const template = {
settings: {
index: {
@@ -31,10 +33,10 @@ export const getSignalsTemplate = (index: string) => {
signal: signalsMapping.mappings.properties.signal,
},
_meta: {
- version,
+ version: SIGNALS_TEMPLATE_VERSION,
},
},
- version,
+ version: SIGNALS_TEMPLATE_VERSION,
};
return template;
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts
index b9ae8b546b8bd..d1b1a2b4dd0eb 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts
@@ -8,7 +8,8 @@ import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
import { transformError, buildSiemResponse } from '../utils';
import { getIndexExists } from '../../index/get_index_exists';
-import { templateNeedsUpdate } from './check_template_version';
+import { SIGNALS_TEMPLATE_VERSION } from './get_signals_template';
+import { getIndexVersion } from './get_index_version';
export const readIndexRoute = (router: IRouter) => {
router.get(
@@ -32,10 +33,24 @@ export const readIndexRoute = (router: IRouter) => {
const index = siemClient.getSignalsIndex();
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index);
- const templateOutdated = await templateNeedsUpdate(clusterClient.callAsCurrentUser, index);
if (indexExists) {
- return response.ok({ body: { name: index, template_outdated: templateOutdated } });
+ let mappingOutdated: boolean | null = null;
+ try {
+ const indexVersion = await getIndexVersion(clusterClient.callAsCurrentUser, index);
+ mappingOutdated = indexVersion !== SIGNALS_TEMPLATE_VERSION;
+ } catch (err) {
+ const error = transformError(err);
+ // Some users may not have the view_index_metadata permission necessary to check the index mapping version
+ // so just continue and return null for index_mapping_outdated if the error is a 403
+ if (error.statusCode !== 403) {
+ return siemResponse.error({
+ body: error.message,
+ statusCode: error.statusCode,
+ });
+ }
+ }
+ return response.ok({ body: { name: index, index_mapping_outdated: mappingOutdated } });
} else {
return siemResponse.error({
statusCode: 404,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
index b1f6f73b09627..f885445c29b04 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IRouter } from '../../../../../../../../src/core/server';
+import { AppClient } from '../../../../types';
+import { IRouter, RequestHandlerContext } from '../../../../../../../../src/core/server';
import { validate } from '../../../../../common/validate';
import {
@@ -28,6 +29,8 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update';
import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules';
import { transformError, buildSiemResponse } from '../utils';
+import { AlertsClient } from '../../../../../../alerts/server';
+import { FrameworkRequest } from '../../../framework';
export const addPrepackedRulesRoute = (
router: IRouter,
@@ -48,62 +51,20 @@ export const addPrepackedRulesRoute = (
try {
const alertsClient = context.alerting?.getAlertsClient();
- const clusterClient = context.core.elasticsearch.legacy.client;
- const savedObjectsClient = context.core.savedObjects.client;
const siemClient = context.securitySolution?.getAppClient();
if (!siemClient || !alertsClient) {
return siemResponse.error({ statusCode: 404 });
}
- // This will create the endpoint list if it does not exist yet
- await context.lists?.getExceptionListClient().createEndpointList();
-
- const rulesFromFileSystem = getPrepackagedRules();
- const prepackagedRules = await getExistingPrepackagedRules({ alertsClient });
- const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules);
- const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules);
- const signalsIndex = siemClient.getSignalsIndex();
- if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) {
- const signalsIndexExists = await getIndexExists(
- clusterClient.callAsCurrentUser,
- signalsIndex
- );
- if (!signalsIndexExists) {
- return siemResponse.error({
- statusCode: 400,
- body: `Pre-packaged rules cannot be installed until the signals index is created: ${signalsIndex}`,
- });
- }
- }
- const result = await Promise.all([
- installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex),
- installPrepackagedTimelines(config.maxTimelineImportExportSize, frameworkRequest, true),
- ]);
- const [prepackagedTimelinesResult, timelinesErrors] = validate(
- result[1],
- importTimelineResultSchema
- );
- await updatePrepackagedRules(alertsClient, savedObjectsClient, rulesToUpdate, signalsIndex);
-
- const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = {
- rules_installed: rulesToInstall.length,
- rules_updated: rulesToUpdate.length,
- timelines_installed: prepackagedTimelinesResult?.timelines_installed ?? 0,
- timelines_updated: prepackagedTimelinesResult?.timelines_updated ?? 0,
- };
- const [validated, genericErrors] = validate(
- prepackagedRulesOutput,
- prePackagedRulesAndTimelinesSchema
+ const validated = await createPrepackagedRules(
+ context,
+ siemClient,
+ alertsClient,
+ frameworkRequest,
+ config.maxTimelineImportExportSize
);
- if (genericErrors != null && timelinesErrors != null) {
- return siemResponse.error({
- statusCode: 500,
- body: [genericErrors, timelinesErrors].filter((msg) => msg != null).join(', '),
- });
- } else {
- return response.ok({ body: validated ?? {} });
- }
+ return response.ok({ body: validated ?? {} });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
@@ -114,3 +75,71 @@ export const addPrepackedRulesRoute = (
}
);
};
+
+class PrepackagedRulesError extends Error {
+ public readonly statusCode: number;
+ constructor(message: string, statusCode: number) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+}
+
+export const createPrepackagedRules = async (
+ context: RequestHandlerContext,
+ siemClient: AppClient,
+ alertsClient: AlertsClient,
+ frameworkRequest: FrameworkRequest,
+ maxTimelineImportExportSize: number
+): Promise => {
+ const clusterClient = context.core.elasticsearch.legacy.client;
+ const savedObjectsClient = context.core.savedObjects.client;
+
+ if (!siemClient || !alertsClient) {
+ throw new PrepackagedRulesError('', 404);
+ }
+
+ // This will create the endpoint list if it does not exist yet
+ await context.lists?.getExceptionListClient().createEndpointList();
+
+ const rulesFromFileSystem = getPrepackagedRules();
+ const prepackagedRules = await getExistingPrepackagedRules({ alertsClient });
+ const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules);
+ const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules);
+ const signalsIndex = siemClient.getSignalsIndex();
+ if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) {
+ const signalsIndexExists = await getIndexExists(clusterClient.callAsCurrentUser, signalsIndex);
+ if (!signalsIndexExists) {
+ throw new PrepackagedRulesError(
+ `Pre-packaged rules cannot be installed until the signals index is created: ${signalsIndex}`,
+ 400
+ );
+ }
+ }
+ const result = await Promise.all([
+ installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex),
+ installPrepackagedTimelines(maxTimelineImportExportSize, frameworkRequest, true),
+ ]);
+ const [prepackagedTimelinesResult, timelinesErrors] = validate(
+ result[1],
+ importTimelineResultSchema
+ );
+ await updatePrepackagedRules(alertsClient, savedObjectsClient, rulesToUpdate, signalsIndex);
+
+ const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = {
+ rules_installed: rulesToInstall.length,
+ rules_updated: rulesToUpdate.length,
+ timelines_installed: prepackagedTimelinesResult?.timelines_installed ?? 0,
+ timelines_updated: prepackagedTimelinesResult?.timelines_updated ?? 0,
+ };
+ const [validated, genericErrors] = validate(
+ prepackagedRulesOutput,
+ prePackagedRulesAndTimelinesSchema
+ );
+ if (genericErrors != null && timelinesErrors != null) {
+ throw new PrepackagedRulesError(
+ [genericErrors, timelinesErrors].filter((msg) => msg != null).join(', '),
+ 500
+ );
+ }
+ return validated;
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts
index 6768e9534a87e..977dad680f8a4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts
@@ -7,7 +7,9 @@
import Boom from 'boom';
import { SavedObjectsFindResponse } from 'kibana/server';
-import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types';
+
+import { alertsClientMock } from '../../../../../alerts/server/mocks';
+import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusSOAttributes } from '../rules/types';
import { BadRequestError } from '../errors/bad_request_error';
import {
transformError,
@@ -19,8 +21,14 @@ import {
transformImportError,
convertToSnakeCase,
SiemResponseFactory,
+ mergeStatuses,
+ getFailingRules,
} from './utils';
import { responseMock } from './__mocks__';
+import { exampleRuleStatus, exampleFindRuleStatusResponse } from '../signals/__mocks__/es_results';
+import { getResult } from './__mocks__/request_responses';
+
+let alertsClient: ReturnType;
describe('utils', () => {
describe('transformError', () => {
@@ -319,7 +327,7 @@ describe('utils', () => {
saved_objects: [],
};
expect(
- convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not
+ convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not
).toEqual(null);
});
});
@@ -350,4 +358,133 @@ describe('utils', () => {
);
});
});
+
+ describe('mergeStatuses', () => {
+ it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => {
+ const statusOne = exampleRuleStatus();
+ statusOne.attributes.status = 'failed';
+ const statusTwo = exampleRuleStatus();
+ statusTwo.attributes.status = 'failed';
+ const currentStatus = exampleRuleStatus();
+ const foundRules = exampleFindRuleStatusResponse([currentStatus, statusOne, statusTwo]);
+ const res = mergeStatuses(currentStatus.attributes.alertId, foundRules.saved_objects, {
+ 'myfakealertid-8cfac': {
+ current_status: {
+ alert_id: 'myfakealertid-8cfac',
+ status_date: '2020-03-27T22:55:59.517Z',
+ status: 'succeeded',
+ last_failure_at: null,
+ last_success_at: '2020-03-27T22:55:59.517Z',
+ last_failure_message: null,
+ last_success_message: 'succeeded',
+ gap: null,
+ bulk_create_time_durations: [],
+ search_after_time_durations: [],
+ last_look_back_date: null,
+ },
+ failures: [],
+ },
+ });
+ expect(res).toEqual({
+ 'myfakealertid-8cfac': {
+ current_status: {
+ alert_id: 'myfakealertid-8cfac',
+ status_date: '2020-03-27T22:55:59.517Z',
+ status: 'succeeded',
+ last_failure_at: null,
+ last_success_at: '2020-03-27T22:55:59.517Z',
+ last_failure_message: null,
+ last_success_message: 'succeeded',
+ gap: null,
+ bulk_create_time_durations: [],
+ search_after_time_durations: [],
+ last_look_back_date: null,
+ },
+ failures: [],
+ },
+ 'f4b8e31d-cf93-4bde-a265-298bde885cd7': {
+ current_status: {
+ alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
+ status_date: '2020-03-27T22:55:59.517Z',
+ status: 'succeeded',
+ last_failure_at: null,
+ last_success_at: '2020-03-27T22:55:59.517Z',
+ last_failure_message: null,
+ last_success_message: 'succeeded',
+ gap: null,
+ bulk_create_time_durations: [],
+ search_after_time_durations: [],
+ last_look_back_date: null,
+ },
+ failures: [
+ {
+ alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
+ status_date: '2020-03-27T22:55:59.517Z',
+ status: 'failed',
+ last_failure_at: null,
+ last_success_at: '2020-03-27T22:55:59.517Z',
+ last_failure_message: null,
+ last_success_message: 'succeeded',
+ gap: null,
+ bulk_create_time_durations: [],
+ search_after_time_durations: [],
+ last_look_back_date: null,
+ },
+ {
+ alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
+ status_date: '2020-03-27T22:55:59.517Z',
+ status: 'failed',
+ last_failure_at: null,
+ last_success_at: '2020-03-27T22:55:59.517Z',
+ last_failure_message: null,
+ last_success_message: 'succeeded',
+ gap: null,
+ bulk_create_time_durations: [],
+ search_after_time_durations: [],
+ last_look_back_date: null,
+ },
+ ],
+ },
+ });
+ });
+ });
+
+ describe('getFailingRules', () => {
+ beforeEach(() => {
+ alertsClient = alertsClientMock.create();
+ });
+ it('getFailingRules finds no failing rules', async () => {
+ alertsClient.get.mockResolvedValue(getResult());
+ const res = await getFailingRules(['my-fake-id'], alertsClient);
+ expect(res).toEqual({});
+ });
+ it('getFailingRules finds a failing rule', async () => {
+ const foundRule = getResult();
+ foundRule.executionStatus = {
+ status: 'error',
+ lastExecutionDate: foundRule.executionStatus.lastExecutionDate,
+ error: {
+ reason: 'read',
+ message: 'oops',
+ },
+ };
+ alertsClient.get.mockResolvedValue(foundRule);
+ const res = await getFailingRules([foundRule.id], alertsClient);
+ expect(res).toEqual({ [foundRule.id]: foundRule });
+ });
+ it('getFailingRules throws an error', async () => {
+ alertsClient.get.mockImplementation(() => {
+ throw new Error('my test error');
+ });
+ let error;
+ try {
+ await getFailingRules(['my-fake-id'], alertsClient);
+ } catch (exc) {
+ error = exc;
+ }
+ expect(error.message).toEqual(
+ 'Failed to get executionStatus with AlertsClient: my test error'
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts
index 96f96d7ebcc9e..72be7a3c0fa08 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts
@@ -17,7 +17,7 @@ import {
} from '../../../../../../../src/core/server';
import { AlertsClient } from '../../../../../alerts/server';
import { BadRequestError } from '../errors/bad_request_error';
-import { RuleStatusResponse, IRuleStatusAttributes } from '../rules/types';
+import { RuleStatusResponse, IRuleStatusSOAttributes } from '../rules/types';
export interface OutputError {
message: string;
@@ -294,39 +294,53 @@ export const convertToSnakeCase = >(
}, {});
};
+/**
+ *
+ * @param id rule id
+ * @param currentStatusAndFailures array of rule statuses where the 0th status is the current status and 1-5 positions are the historical failures
+ * @param acc accumulated rule id : statuses
+ */
export const mergeStatuses = (
id: string,
- failures: Array>,
+ currentStatusAndFailures: Array>,
acc: RuleStatusResponse
-) => {
- if (failures.length === 0) {
+): RuleStatusResponse => {
+ if (currentStatusAndFailures.length === 0) {
return {
...acc,
};
}
- const convertedCurrentStatus = convertToSnakeCase(failures[0].attributes);
+ const convertedCurrentStatus = convertToSnakeCase(
+ currentStatusAndFailures[0].attributes
+ );
return {
...acc,
[id]: {
current_status: convertedCurrentStatus,
- failures: failures.map((errorItem) =>
- convertToSnakeCase(errorItem.attributes)
- ),
+ failures: currentStatusAndFailures
+ .slice(1)
+ .map((errorItem) => convertToSnakeCase(errorItem.attributes)),
},
} as RuleStatusResponse;
};
-export const getFailingRules = (ids: string[], alertsClient: AlertsClient) =>
- Promise.all(
- ids.map(async (id) =>
- alertsClient.get({
- id,
- })
- )
- )
- .then((rules) => rules.filter((rule) => rule.executionStatus.status === 'error'))
- .then((rules) =>
- rules.reduce((acc, failingRule) => {
+export type GetFailingRulesResult = Record;
+
+export const getFailingRules = async (
+ ids: string[],
+ alertsClient: AlertsClient
+): Promise => {
+ try {
+ const errorRules = await Promise.all(
+ ids.map(async (id) =>
+ alertsClient.get({
+ id,
+ })
+ )
+ );
+ return errorRules
+ .filter((rule) => rule.executionStatus.status === 'error')
+ .reduce((acc, failingRule) => {
const accum = acc;
const theRule = failingRule;
return {
@@ -335,5 +349,8 @@ export const getFailingRules = (ids: string[], alertsClient: AlertsClient) =>
},
...accum,
};
- }, {} as Record)
- );
+ }, {});
+ } catch (exc) {
+ throw new Error(`Failed to get executionStatus with AlertsClient: ${exc.message}`);
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
index 8af622e6a128b..fb4763a982f43 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
@@ -105,7 +105,7 @@ export interface RuleAlertType extends Alert {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export interface IRuleStatusAttributes extends Record {
+export interface IRuleStatusSOAttributes extends Record {
alertId: string; // created alert id.
statusDate: StatusDate;
lastFailureAt: LastFailureAt | null | undefined;
@@ -119,21 +119,35 @@ export interface IRuleStatusAttributes extends Record {
searchAfterTimeDurations: string[] | null | undefined;
}
+export interface IRuleStatusResponseAttributes {
+ alert_id: string; // created alert id.
+ status_date: StatusDate;
+ last_failure_at: LastFailureAt | null | undefined;
+ last_failure_message: LastFailureMessage | null | undefined;
+ last_success_at: LastSuccessAt | null | undefined;
+ last_success_message: LastSuccessMessage | null | undefined;
+ status: JobStatus | null | undefined;
+ last_look_back_date: string | null | undefined;
+ gap: string | null | undefined;
+ bulk_create_time_durations: string[] | null | undefined;
+ search_after_time_durations: string[] | null | undefined;
+}
+
export interface RuleStatusResponse {
[key: string]: {
- current_status: IRuleStatusAttributes | null | undefined;
- failures: IRuleStatusAttributes[] | null | undefined;
+ current_status: IRuleStatusResponseAttributes | null | undefined;
+ failures: IRuleStatusResponseAttributes[] | null | undefined;
};
}
export interface IRuleSavedAttributesSavedObjectAttributes
- extends IRuleStatusAttributes,
+ extends IRuleStatusSOAttributes,
SavedObjectAttributes {}
export interface IRuleStatusSavedObject {
type: string;
id: string;
- attributes: Array>;
+ attributes: Array>;
references: unknown[];
updated_at: string;
version: string;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index cbf70f3119b31..4559a658c9583 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -19,7 +19,7 @@ import {
} from '../../../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks';
import { RuleTypeParams } from '../../types';
-import { IRuleStatusAttributes } from '../../rules/types';
+import { IRuleStatusSOAttributes } from '../../rules/types';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { RulesSchema } from '../../../../../common/detection_engine/schemas/response';
@@ -555,7 +555,7 @@ export const sampleDocSearchResultsWithSortId = (
export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd';
export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a';
-export const exampleRuleStatus: () => SavedObject = () => ({
+export const exampleRuleStatus: () => SavedObject = () => ({
type: ruleStatusSavedObjectType,
id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e',
attributes: {
@@ -577,8 +577,10 @@ export const exampleRuleStatus: () => SavedObject = () =>
});
export const exampleFindRuleStatusResponse: (
- mockStatuses: Array>
-) => SavedObjectsFindResponse = (mockStatuses = [exampleRuleStatus()]) => ({
+ mockStatuses: Array>
+) => SavedObjectsFindResponse = (
+ mockStatuses = [exampleRuleStatus()]
+) => ({
total: 1,
per_page: 6,
page: 1,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts
index 913efbe04aa16..1ddec9cd15148 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts
@@ -6,7 +6,7 @@
import { SavedObject } from 'src/core/server';
-import { IRuleStatusAttributes } from '../rules/types';
+import { IRuleStatusSOAttributes } from '../rules/types';
import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects';
@@ -18,7 +18,7 @@ interface RuleStatusParams {
export const createNewRuleStatus = async ({
alertId,
ruleStatusClient,
-}: RuleStatusParams): Promise> => {
+}: RuleStatusParams): Promise> => {
const now = new Date().toISOString();
return ruleStatusClient.create({
alertId,
@@ -38,7 +38,7 @@ export const createNewRuleStatus = async ({
export const getOrCreateRuleStatuses = async ({
alertId,
ruleStatusClient,
-}: RuleStatusParams): Promise>> => {
+}: RuleStatusParams): Promise>> => {
const ruleStatuses = await getRuleStatusSavedObjects({
alertId,
ruleStatusClient,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts
index 828b4ea41096e..72a271fb2606f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts
@@ -5,7 +5,7 @@
*/
import { SavedObjectsFindResponse } from 'kibana/server';
-import { IRuleStatusAttributes } from '../rules/types';
+import { IRuleStatusSOAttributes } from '../rules/types';
import { MAX_RULE_STATUSES } from './rule_status_service';
import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
@@ -17,7 +17,7 @@ interface GetRuleStatusSavedObject {
export const getRuleStatusSavedObjects = async ({
alertId,
ruleStatusClient,
-}: GetRuleStatusSavedObject): Promise> => {
+}: GetRuleStatusSavedObject): Promise> => {
return ruleStatusClient.find({
perPage: MAX_RULE_STATUSES,
sortField: 'statusDate',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts
index 4b5faeb5b9d27..f6a08852ac8d5 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts
@@ -12,17 +12,17 @@ import {
SavedObjectsFindResponse,
} from '../../../../../../../src/core/server';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
-import { IRuleStatusAttributes } from '../rules/types';
+import { IRuleStatusSOAttributes } from '../rules/types';
export interface RuleStatusSavedObjectsClient {
find: (
options?: Omit
- ) => Promise>;
- create: (attributes: IRuleStatusAttributes) => Promise>;
+ ) => Promise>;
+ create: (attributes: IRuleStatusSOAttributes) => Promise>;
update: (
id: string,
- attributes: Partial
- ) => Promise>;
+ attributes: Partial
+ ) => Promise>;
delete: (id: string) => Promise<{}>;
}
@@ -30,7 +30,10 @@ export const ruleStatusSavedObjectsClientFactory = (
savedObjectsClient: SavedObjectsClientContract
): RuleStatusSavedObjectsClient => ({
find: (options) =>
- savedObjectsClient.find({ ...options, type: ruleStatusSavedObjectType }),
+ savedObjectsClient.find({
+ ...options,
+ type: ruleStatusSavedObjectType,
+ }),
create: (attributes) => savedObjectsClient.create(ruleStatusSavedObjectType, attributes),
update: (id, attributes) => savedObjectsClient.update(ruleStatusSavedObjectType, id, attributes),
delete: (id) => savedObjectsClient.delete(ruleStatusSavedObjectType, id),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
index 8fdbe282eece5..433ad4e2affea 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
@@ -6,7 +6,7 @@
import { assertUnreachable } from '../../../../common/utility_types';
import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas';
-import { IRuleStatusAttributes } from '../rules/types';
+import { IRuleStatusSOAttributes } from '../rules/types';
import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses';
import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
@@ -30,9 +30,9 @@ export const buildRuleStatusAttributes: (
status: JobStatus,
message?: string,
attributes?: Attributes
-) => Partial = (status, message, attributes = {}) => {
+) => Partial = (status, message, attributes = {}) => {
const now = new Date().toISOString();
- const baseAttributes: Partial = {
+ const baseAttributes: Partial = {
...attributes,
status,
statusDate: now,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index 838ac2558b038..bb3a0b4fa6f08 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -8,7 +8,6 @@
import { Logger, KibanaRequest } from 'src/core/server';
-import { get } from 'lodash';
import {
SIGNALS_ID,
DEFAULT_SEARCH_AFTER_PAGE_SIZE,
@@ -62,6 +61,8 @@ import { buildEqlSearchRequest } from '../../../../common/detection_engine/get_q
import { bulkInsertSignals } from './single_bulk_create';
import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk_body';
import { createThreatSignals } from './threat_mapping/create_threat_signals';
+import { getIndexVersion } from '../routes/index/get_index_version';
+import { MIN_EQL_RULE_INDEX_VERSION } from '../routes/index/get_signals_template';
export const signalRulesAlertType = ({
logger,
@@ -119,17 +120,6 @@ export const signalRulesAlertType = ({
type,
exceptionsList,
} = params;
- const outputIndexTemplateMapping: unknown = await services.callCluster(
- 'indices.getTemplate',
- { name: outputIndex }
- );
- const signalMappingVersion: number | undefined = get(outputIndexTemplateMapping, [
- outputIndex,
- 'version',
- ]);
- if (signalMappingVersion !== undefined && typeof signalMappingVersion !== 'number') {
- throw new Error('Found non-numeric value for "version" in output index template');
- }
const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE);
let hasError: boolean = false;
@@ -457,14 +447,24 @@ export const signalRulesAlertType = ({
if (query === undefined) {
throw new Error('EQL query rule must have a query defined');
}
- const MIN_EQL_RULE_TEMPLATE_VERSION = 2;
- if (
- signalMappingVersion === undefined ||
- signalMappingVersion < MIN_EQL_RULE_TEMPLATE_VERSION
- ) {
- throw new Error(
- `EQL based rules require an update to version ${MIN_EQL_RULE_TEMPLATE_VERSION} of the detection alerts index mapping`
- );
+ try {
+ const signalIndexVersion = await getIndexVersion(services.callCluster, outputIndex);
+ if (
+ signalIndexVersion === undefined ||
+ signalIndexVersion < MIN_EQL_RULE_INDEX_VERSION
+ ) {
+ throw new Error(
+ `EQL based rules require an update to version ${MIN_EQL_RULE_INDEX_VERSION} of the detection alerts index mapping`
+ );
+ }
+ } catch (err) {
+ if (err.statusCode === 403) {
+ throw new Error(
+ `EQL based rules require the user that created it to have the view_index_metadata, read, and write permissions for index: ${outputIndex}`
+ );
+ } else {
+ throw err;
+ }
}
const inputIndex = await getInputIndex(services, version, index);
const request = buildEqlSearchRequest(
diff --git a/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts b/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts
index 6d9e9b13bc356..e36fb1144e93f 100644
--- a/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts
+++ b/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts
@@ -149,14 +149,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter {
}
public getIndexPatternsService(request: FrameworkRequest): FrameworkIndexPatternsService {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const callCluster = async (endpoint: string, params?: Record) =>
- this.callWithRequest(request, endpoint, {
- ...params,
- allowNoIndices: true,
- });
-
- return new IndexPatternsFetcher(callCluster);
+ return new IndexPatternsFetcher(request.context.core.elasticsearch.client.asCurrentUser, true);
}
}
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index f5e1c6936cbd6..43f87a0c69ab3 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -23,7 +23,10 @@ import {
PluginStart as DataPluginStart,
} from '../../../../src/plugins/data/server';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
-import { PluginSetupContract as AlertingSetup } from '../../alerts/server';
+import {
+ PluginSetupContract as AlertingSetup,
+ PluginStartContract as AlertPluginStartContract,
+} from '../../alerts/server';
import { SecurityPluginSetup as SecuritySetup } from '../../security/server';
import { PluginSetupContract as FeaturesSetup } from '../../features/server';
import { MlPluginSetup as MlSetup } from '../../ml/server';
@@ -88,6 +91,7 @@ export interface SetupPlugins {
}
export interface StartPlugins {
+ alerts: AlertPluginStartContract;
data: DataPluginStart;
ingestManager?: IngestManagerStartContract;
taskManager?: TaskManagerStartContract;
@@ -113,8 +117,10 @@ const securitySubPlugins = [
export class Plugin implements IPlugin {
private readonly logger: Logger;
private readonly config$: Observable;
+ private config?: ConfigType;
private context: PluginInitializerContext;
private appClientFactory: AppClientFactory;
+ private setupPlugins?: SetupPlugins;
private readonly endpointAppContextService = new EndpointAppContextService();
private readonly telemetryEventsSender: TelemetryEventsSender;
@@ -137,8 +143,10 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) {
this.logger.debug('plugin setup');
+ this.setupPlugins = plugins;
const config = await this.config$.pipe(first()).toPromise();
+ this.config = config;
const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise();
initSavedObjects(core.savedObjects);
@@ -337,6 +345,10 @@ export class Plugin implements IPlugin(async (resolve) => {
const { elasticsearch } = context.core;
- const indexPatternsFetcher = new IndexPatternsFetcher(
- elasticsearch.legacy.client.callAsCurrentUser
- );
+ const indexPatternsFetcher = new IndexPatternsFetcher(elasticsearch.client.asCurrentUser);
const dedupeIndices = dedupeIndexName(request.indices);
const responsesIndexFields = await Promise.all(
diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts
index 8d47d3dd30b82..a40df3b84132e 100644
--- a/x-pack/plugins/task_manager/server/task_store.test.ts
+++ b/x-pack/plugins/task_manager/server/task_store.test.ts
@@ -367,7 +367,7 @@ describe('TaskStore', () => {
const {
args: {
- updateByQuery: { body: { query } = {} },
+ updateByQuery: { body: { query, sort } = {} },
},
} = await testClaimAvailableTasks({
opts: {
@@ -476,6 +476,25 @@ describe('TaskStore', () => {
],
},
});
+ expect(sort).toMatchObject([
+ {
+ _script: {
+ type: 'number',
+ order: 'asc',
+ script: {
+ lang: 'painless',
+ source: `
+if (doc['task.retryAt'].size()!=0) {
+ return doc['task.retryAt'].value.toInstant().toEpochMilli();
+}
+if (doc['task.runAt'].size()!=0) {
+ return doc['task.runAt'].value.toInstant().toEpochMilli();
+}
+ `,
+ },
+ },
+ },
+ ]);
});
test('it supports claiming specific tasks by id', async () => {
diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts
index bdeea45ef1d94..7f231731db01f 100644
--- a/x-pack/plugins/task_manager/server/task_store.ts
+++ b/x-pack/plugins/task_manager/server/task_store.ts
@@ -46,6 +46,7 @@ import {
RangeFilter,
asPinnedQuery,
matchesClauses,
+ SortOptions,
} from './queries/query_clauses';
import {
@@ -277,6 +278,17 @@ export class TaskStore {
)
);
+ // The documents should be sorted by runAt/retryAt, unless there are pinned
+ // tasks being queried, in which case we want to sort by score first, and then
+ // the runAt/retryAt. That way we'll get the pinned tasks first. Note that
+ // the score seems to favor newer documents rather than older documents, so
+ // if there are not pinned tasks being queried, we do NOT want to sort by score
+ // at all, just by runAt/retryAt.
+ const sort: SortOptions = [SortByRunAtAndRetryAt];
+ if (claimTasksById && claimTasksById.length) {
+ sort.unshift('_score');
+ }
+
const apmTrans = apm.startTransaction(`taskManager markAvailableTasksAsClaimed`, 'taskManager');
const { updated } = await this.updateByQuery(
asUpdateByQuery({
@@ -293,12 +305,7 @@ export class TaskStore {
status: 'claiming',
retryAt: claimOwnershipUntil,
}),
- sort: [
- // sort by score first, so the "pinned" Tasks are first
- '_score',
- // the nsort by other fields
- SortByRunAtAndRetryAt,
- ],
+ sort,
}),
{
max_docs: size,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index e645ae32abbd1..5cabdd62d7c87 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -10996,11 +10996,9 @@
"xpack.lens.xyChart.topAxisLabel": "上の軸",
"xpack.lens.xyChart.valuesLabel": "値",
"xpack.lens.xyChart.xAxisGridlines.help": "x軸のグリッド線を表示するかどうかを指定します。",
- "xpack.lens.xyChart.xAxisLabel": "X 軸",
"xpack.lens.xyChart.xAxisTickLabels.help": "x軸の目盛ラベルを表示するかどうかを指定します。",
"xpack.lens.xyChart.xAxisTitle.help": "x軸のタイトルを表示するかどうかを指定します。",
"xpack.lens.xyChart.xTitle.help": "x軸のタイトル",
- "xpack.lens.xyChart.yAxisLabel": "Y 軸",
"xpack.lens.xyChart.yLeftAxisgridlines.help": "左y軸のグリッド線を表示するかどうかを指定します。",
"xpack.lens.xyChart.yLeftAxisTickLabels.help": "左y軸の目盛ラベルを表示するかどうかを指定します。",
"xpack.lens.xyChart.yLeftAxisTitle.help": "左y軸のタイトルを表示するかどうかを指定します。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 2f701d0cde284..229938a3c1d08 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -11009,11 +11009,9 @@
"xpack.lens.xyChart.topAxisLabel": "顶轴",
"xpack.lens.xyChart.valuesLabel": "值",
"xpack.lens.xyChart.xAxisGridlines.help": "指定 x 轴的网格线是否可见。",
- "xpack.lens.xyChart.xAxisLabel": "X 轴",
"xpack.lens.xyChart.xAxisTickLabels.help": "指定 x 轴的刻度标签是否可见。",
"xpack.lens.xyChart.xAxisTitle.help": "指定 x 轴的标题是否可见。",
"xpack.lens.xyChart.xTitle.help": "X 轴标题",
- "xpack.lens.xyChart.yAxisLabel": "Y 轴",
"xpack.lens.xyChart.yLeftAxisgridlines.help": "指定左侧 y 轴的网格线是否可见。",
"xpack.lens.xyChart.yLeftAxisTickLabels.help": "指定左侧 y 轴的刻度标签是否可见。",
"xpack.lens.xyChart.yLeftAxisTitle.help": "指定左侧 y 轴的标题是否可见。",
diff --git a/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx
index 16853211433ca..18e058e305606 100644
--- a/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx
+++ b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx
@@ -6,7 +6,16 @@
import React from 'react';
import { UptimeDatePicker } from '../uptime_date_picker';
-import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib';
+import {
+ renderWithRouter,
+ shallowWithRouter,
+ MountWithReduxProvider,
+ mountWithRouterRedux,
+} from '../../../lib';
+import { UptimeStartupPluginsContextProvider } from '../../../contexts';
+import { startPlugins } from '../../../lib/__mocks__/uptime_plugin_start_mock';
+import { ClientPluginsStart } from '../../../apps/plugin';
+import { createMemoryHistory } from 'history';
describe('UptimeDatePicker component', () => {
it('validates props with shallow render', () => {
@@ -22,4 +31,59 @@ describe('UptimeDatePicker component', () => {
);
expect(component).toMatchSnapshot();
});
+
+ it('uses shared date range state when there is no url date range state', () => {
+ const customHistory = createMemoryHistory();
+ jest.spyOn(customHistory, 'push');
+
+ const component = mountWithRouterRedux(
+ )}
+ >
+
+ ,
+ { customHistory }
+ );
+
+ const startBtn = component.find('[data-test-subj="superDatePickerstartDatePopoverButton"]');
+
+ expect(startBtn.text()).toBe('~ 30 minutes ago');
+
+ const endBtn = component.find('[data-test-subj="superDatePickerendDatePopoverButton"]');
+
+ expect(endBtn.text()).toBe('~ 15 minutes ago');
+
+ expect(customHistory.push).toHaveBeenCalledWith({
+ pathname: '/',
+ search: 'dateRangeStart=now-30m&dateRangeEnd=now-15m',
+ });
+ });
+
+ it('should use url date range even if shared date range is present', () => {
+ const customHistory = createMemoryHistory({
+ initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'],
+ });
+
+ jest.spyOn(customHistory, 'push');
+
+ const component = mountWithRouterRedux(
+ )}
+ >
+
+ ,
+ { customHistory }
+ );
+
+ const showDateBtn = component.find('[data-test-subj="superDatePickerShowDatesButton"]');
+
+ expect(showDateBtn.childAt(0).text()).toBe('Last 10 minutes');
+
+ // it should update shared state
+
+ expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({
+ from: 'now-10m',
+ to: 'now',
+ });
+ });
});
diff --git a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx
index 1d0dcad73795b..cc8d6271abd73 100644
--- a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx
+++ b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx
@@ -4,11 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext } from 'react';
+import React, { useContext, useEffect } from 'react';
import { EuiSuperDatePicker } from '@elastic/eui';
import { useUrlParams } from '../../hooks';
import { CLIENT_DEFAULTS } from '../../../common/constants';
-import { UptimeRefreshContext, UptimeSettingsContext } from '../../contexts';
+import {
+ UptimeRefreshContext,
+ UptimeSettingsContext,
+ UptimeStartupPluginsContext,
+} from '../../contexts';
export interface CommonlyUsedRange {
from: string;
@@ -16,12 +20,43 @@ export interface CommonlyUsedRange {
display: string;
}
+const isUptimeDefaultDateRange = (dateRangeStart: string, dateRangeEnd: string) => {
+ const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS;
+
+ return dateRangeStart === DATE_RANGE_START && dateRangeEnd === DATE_RANGE_END;
+};
+
export const UptimeDatePicker = () => {
const [getUrlParams, updateUrl] = useUrlParams();
- const { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd } = getUrlParams();
const { commonlyUsedRanges } = useContext(UptimeSettingsContext);
const { refreshApp } = useContext(UptimeRefreshContext);
+ const { data } = useContext(UptimeStartupPluginsContext);
+
+ // read time from state and update the url
+ const sharedTimeState = data?.query.timefilter.timefilter.getTime();
+
+ const {
+ autorefreshInterval,
+ autorefreshIsPaused,
+ dateRangeStart: start,
+ dateRangeEnd: end,
+ } = getUrlParams();
+
+ useEffect(() => {
+ const { from, to } = sharedTimeState ?? {};
+ // if it's uptime default range, and we have shared state from kibana, let's use that
+ if (isUptimeDefaultDateRange(start, end) && (from !== start || to !== end)) {
+ updateUrl({ dateRangeStart: from, dateRangeEnd: to });
+ } else if (from !== start || to !== end) {
+ // if it's coming url. let's update shared state
+ data?.query.timefilter.timefilter.setTime({ from: start, to: end });
+ }
+
+ // only need at start, rest date picker on change fucn will take care off
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const euiCommonlyUsedRanges = commonlyUsedRanges
? commonlyUsedRanges.map(
({ from, to, display }: { from: string; to: string; display: string }) => {
@@ -36,13 +71,17 @@ export const UptimeDatePicker = () => {
return (
{
- updateUrl({ dateRangeStart: start, dateRangeEnd: end });
+ onTimeChange={({ start: startN, end: endN }) => {
+ if (data?.query?.timefilter?.timefilter) {
+ data?.query.timefilter.timefilter.setTime({ from: startN, to: endN });
+ }
+
+ updateUrl({ dateRangeStart: startN, dateRangeEnd: endN });
refreshApp();
}}
onRefresh={refreshApp}
diff --git a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts
new file mode 100644
index 0000000000000..6d2ea80a3b6f2
--- /dev/null
+++ b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.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.
+ */
+
+interface InputTimeRange {
+ from: string;
+ to: string;
+}
+
+export const startPlugins = {
+ data: {
+ query: {
+ timefilter: {
+ timefilter: {
+ getTime: () => ({ to: 'now-15m', from: 'now-30m' }),
+ setTime: jest.fn(({ from, to }: InputTimeRange) => {}),
+ },
+ },
+ },
+ },
+};
diff --git a/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx
index 7da570e909425..5219fb3242539 100644
--- a/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx
+++ b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx
@@ -20,22 +20,19 @@ const helperWithRouter: (
wrapReduxStore?: boolean,
storeState?: AppState
) => R = (helper, component, customHistory, wrapReduxStore, storeState) => {
- if (customHistory) {
- customHistory.location.key = 'TestKeyForTesting';
- return helper({component});
- }
- const history = createMemoryHistory();
+ const history = customHistory ?? createMemoryHistory();
+
history.location.key = 'TestKeyForTesting';
+ const routerWrapper = {component};
+
if (wrapReduxStore) {
return helper(
-
- {component}
-
+ {routerWrapper}
);
}
- return helper({component});
+ return helper(routerWrapper);
};
export const renderWithRouter = (component: ReactElement, customHistory?: MemoryHistory) => {
diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
index 3fc26811d46eb..7feb916046e3a 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
@@ -7,6 +7,7 @@
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import Mustache from 'mustache';
+import { ElasticsearchClient } from 'kibana/server';
import { UptimeAlertTypeFactory } from './types';
import { esKuery } from '../../../../../../src/plugins/data/server';
import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common';
@@ -81,6 +82,7 @@ export const generateFilterDSL = async (
export const formatFilterString = async (
dynamicSettings: DynamicSettings,
callES: ESAPICaller,
+ esClient: ElasticsearchClient,
filters: StatusCheckFilters,
search: string,
libs?: UMServerLibs
@@ -88,9 +90,10 @@ export const formatFilterString = async (
await generateFilterDSL(
() =>
libs?.requests?.getIndexPattern
- ? libs?.requests?.getIndexPattern({ callES, dynamicSettings })
+ ? libs?.requests?.getIndexPattern({ callES, esClient, dynamicSettings })
: getUptimeIndexPattern({
callES,
+ esClient,
dynamicSettings,
}),
filters,
@@ -237,6 +240,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
async executor(
{ params: rawParams, state, services: { alertInstanceFactory } },
callES,
+ esClient,
dynamicSettings
) {
const {
@@ -252,7 +256,14 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
timerange: oldVersionTimeRange,
} = rawParams;
- const filterString = await formatFilterString(dynamicSettings, callES, filters, search, libs);
+ const filterString = await formatFilterString(
+ dynamicSettings,
+ callES,
+ esClient,
+ filters,
+ search,
+ libs
+ );
const timerange = oldVersionTimeRange || {
from: isAutoGenerated
diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
index b8a56405ca160..390b6d347996c 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { ILegacyScopedClusterClient, ElasticsearchClient } from 'kibana/server';
import { AlertExecutorOptions, AlertType, AlertTypeState } from '../../../../alerts/server';
import { savedObjectsAdapter } from '../saved_objects';
import { DynamicSettings } from '../../../common/runtime_types';
@@ -13,6 +13,7 @@ export interface UptimeAlertType extends Omit Promise;
}
@@ -22,13 +23,13 @@ export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({
producer: 'uptime',
executor: async (options: AlertExecutorOptions) => {
const {
- services: { callCluster: callES },
+ services: { callCluster: callES, scopedClusterClient },
} = options;
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
options.services.savedObjectsClient
);
- return uptimeAlert.executor(options, callES, dynamicSettings);
+ return uptimeAlert.executor(options, callES, scopedClusterClient, dynamicSettings);
},
});
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts
index 1d284143a1ab0..06846a73ed3d7 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LegacyAPICaller, LegacyCallAPIOptions } from 'src/core/server';
+import { ElasticsearchClient } from 'kibana/server';
import { UMElasticsearchQueryFn } from '../adapters';
import { IndexPatternsFetcher, FieldDescriptor } from '../../../../../../src/plugins/data/server';
@@ -14,15 +14,10 @@ export interface IndexPatternTitleAndFields {
}
export const getUptimeIndexPattern: UMElasticsearchQueryFn<
- {},
+ { esClient: ElasticsearchClient },
IndexPatternTitleAndFields | undefined
-> = async ({ callES, dynamicSettings }) => {
- const callAsCurrentUser: LegacyAPICaller = async (
- endpoint: string,
- clientParams: Record = {},
- options?: LegacyCallAPIOptions
- ) => callES(endpoint, clientParams, options);
- const indexPatternsFetcher = new IndexPatternsFetcher(callAsCurrentUser);
+> = async ({ esClient, dynamicSettings }) => {
+ const indexPatternsFetcher = new IndexPatternsFetcher(esClient);
// Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint)
// and since `getFieldsForWildcard` will throw if the specified indices don't exist,
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts
index fbcbc37ae0cc2..ec750f92656b2 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ElasticsearchClient } from 'kibana/server';
import { ESAPICaller, UMElasticsearchQueryFn } from '../adapters';
import { MonitorDetails, MonitorError } from '../../../common/runtime_types';
import { formatFilterString } from '../alerts/status_check';
@@ -13,10 +14,12 @@ export interface GetMonitorDetailsParams {
dateStart: string;
dateEnd: string;
alertsClient: any;
+ esClient: ElasticsearchClient;
}
const getMonitorAlerts = async (
callES: ESAPICaller,
+ esClient: ElasticsearchClient,
dynamicSettings: any,
alertsClient: any,
monitorId: string
@@ -67,6 +70,7 @@ const getMonitorAlerts = async (
const parsedFilters = await formatFilterString(
dynamicSettings,
callES,
+ esClient,
currAlert.params.filters,
currAlert.params.search
);
@@ -84,7 +88,7 @@ const getMonitorAlerts = async (
export const getMonitorDetails: UMElasticsearchQueryFn<
GetMonitorDetailsParams,
MonitorDetails
-> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => {
+> = async ({ callES, esClient, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => {
const queryFilters: any = [
{
range: {
@@ -134,7 +138,13 @@ export const getMonitorDetails: UMElasticsearchQueryFn<
const monitorError: MonitorError | undefined = data?.error;
const errorTimestamp: string | undefined = data?.['@timestamp'];
- const monAlerts = await getMonitorAlerts(callES, dynamicSettings, alertsClient, monitorId);
+ const monAlerts = await getMonitorAlerts(
+ callES,
+ esClient,
+ dynamicSettings,
+ alertsClient,
+ monitorId
+ );
return {
monitorId,
error: monitorError,
diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts
index 26715f0ff37b6..baf999158a29e 100644
--- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts
+++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts
@@ -16,7 +16,11 @@ export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServer
try {
return response.ok({
body: {
- ...(await libs.requests.getIndexPattern({ callES, dynamicSettings })),
+ ...(await libs.requests.getIndexPattern({
+ callES,
+ esClient: _context.core.elasticsearch.client.asCurrentUser,
+ dynamicSettings,
+ })),
},
});
} catch (e) {
diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts
index bb54effc0d57e..8bbb4fcb5575c 100644
--- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts
+++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts
@@ -28,6 +28,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ
body: {
...(await libs.requests.getMonitorDetails({
callES,
+ esClient: context.core.elasticsearch.client.asCurrentUser,
dynamicSettings,
monitorId,
dateStart,
diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js
index 8dbd98ed3af2f..6a2c1f8437698 100644
--- a/x-pack/test/functional/apps/maps/discover.js
+++ b/x-pack/test/functional/apps/maps/discover.js
@@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }) {
expect(doesLayerExist).to.equal(true);
const hits = await PageObjects.maps.getHits();
expect(hits).to.equal('4');
+ await PageObjects.maps.refreshAndClearUnsavedChangesWarning();
});
it('should link geo_point fields to Maps application with time and query context', async () => {
@@ -55,6 +56,7 @@ export default function ({ getService, getPageObjects }) {
expect(doesLayerExist).to.equal(true);
const hits = await PageObjects.maps.getHits();
expect(hits).to.equal('7');
+ await PageObjects.maps.refreshAndClearUnsavedChangesWarning();
});
});
}
diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js
index bd5ecfe2a2504..0e2850dafbccc 100644
--- a/x-pack/test/functional/apps/maps/joins.js
+++ b/x-pack/test/functional/apps/maps/joins.js
@@ -38,6 +38,7 @@ export default function ({ getPageObjects, getService }) {
after(async () => {
await inspector.close();
+ await PageObjects.maps.refreshAndClearUnsavedChangesWarning();
await security.testUser.restoreDefaults();
});
diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/layer_visibility.js
index dd9b93c995695..75a0e7da0f256 100644
--- a/x-pack/test/functional/apps/maps/layer_visibility.js
+++ b/x-pack/test/functional/apps/maps/layer_visibility.js
@@ -19,6 +19,7 @@ export default function ({ getPageObjects, getService }) {
afterEach(async () => {
await inspector.close();
+ await PageObjects.maps.refreshAndClearUnsavedChangesWarning();
await security.testUser.restoreDefaults();
});
diff --git a/x-pack/test/functional/apps/maps/vector_styling.js b/x-pack/test/functional/apps/maps/vector_styling.js
index 1def542982dd8..e4c5eaf892c76 100644
--- a/x-pack/test/functional/apps/maps/vector_styling.js
+++ b/x-pack/test/functional/apps/maps/vector_styling.js
@@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.maps.loadSavedMap('document example');
});
after(async () => {
+ await PageObjects.maps.refreshAndClearUnsavedChangesWarning();
await security.testUser.restoreDefaults();
});
diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts
index 7be0aa425509e..408a50be8882e 100644
--- a/x-pack/test/functional/page_objects/gis_page.ts
+++ b/x-pack/test/functional/page_objects/gis_page.ts
@@ -19,6 +19,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
const queryBar = getService('queryBar');
const comboBox = getService('comboBox');
const renderable = getService('renderable');
+ const browser = getService('browser');
function escapeLayerName(layerName: string) {
return layerName.split(' ').join('_');
@@ -692,6 +693,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
}
await testSubjects.click('mapSettingSubmitButton');
}
+
+ async refreshAndClearUnsavedChangesWarning() {
+ await browser.refresh();
+ // accept alert if it pops up
+ const alert = await browser.getAlert();
+ await alert?.accept();
+ }
}
return new GisPage();
}
diff --git a/yarn.lock b/yarn.lock
index e2f5ed412a14a..18f868440f508 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1218,10 +1218,10 @@
dependencies:
"@elastic/apm-rum-core" "^5.7.0"
-"@elastic/charts@23.2.1":
- version "23.2.1"
- resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-23.2.1.tgz#1f48629fe4597655a7f119fd019c4d5a2cbaf252"
- integrity sha512-L2jUPAWwE0xLry6DcqcngVLCa9R32pfz5jW1fyOJRWSq1Fay2swOw4joBe8PmHpvl2s8EwWi9qWBORR1z3hUeQ==
+"@elastic/charts@24.0.0":
+ version "24.0.0"
+ resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-24.0.0.tgz#7b97b00a3dc873f46f764de0f28573e236b76aa7"
+ integrity sha512-ZFIdHcU48Wes7eb1R+48L7xLH4p7D9oSdkoX/iuwt+znD353UhiYK9u+dbrpMXeOMtFYt7dktzVAbouHcJCZPA==
dependencies:
"@popperjs/core" "^2.4.0"
chroma-js "^2.1.0"
@@ -9820,10 +9820,10 @@ cypress-promise@^1.1.0:
resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25"
integrity sha512-DhIf5PJ/a0iY+Yii6n7Rbwq+9TJxU4pupXYzf9mZd8nPG0AzQrj9i+pqINv4xbI2EV1p+PKW3maCkR7oPG4GrA==
-cypress@^5.0.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.2.0.tgz#6902efd90703242a2539f0623c6e1118aff01f95"
- integrity sha512-9S2spcrpIXrQ+CQIKHsjRoLQyRc2ehB06clJXPXXp1zyOL/uZMM3Qc20ipNki4CcNwY0nBTQZffPbRpODeGYQg==
+cypress@5.4.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.4.0.tgz#8833a76e91129add601f823d43c53eb512d162c5"
+ integrity sha512-BJR+u3DRSYMqaBS1a3l1rbh5AkMRHugbxcYYzkl+xYlO6dzcJVE8uAhghzVI/hxijCyBg1iuSe4TRp/g1PUg8Q==
dependencies:
"@cypress/listr-verbose-renderer" "^0.4.1"
"@cypress/request" "^2.88.5"