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"